(←) предыдущая запись ; следующая запись (→)
Теперь вернёмся к вопросу про нотацию и именование сущностей.
Мы разобрали три типа наследования параметризованных типов:
— ковариантное (Author<Poem> ← Author<Text>
),
— контравариантное (Reader<Text> ← Reader<Poem>
),
— инвариантное (Set<Poem> ≠ Set<Text>
)
Но теперь языку программирования необходимо объяснить, как именно должен вести себя ваш класс: как Author
, Reader
или Set
?
И в этом месте языки поступают по-разному.
В языке Java программист описывает отношения наследования между базовыми типами:
— Set<Poem>
и Set<Text>
— два инвариантных типа. Всё так просто: по-умолчанию все дженерики инвариантны. Если мы ничего не говорим про наследование типов, то язык не пытается делать догадок за нас.
— Author<T extends Text>
— тип T
расширяет тип Text
(то есть является подтипом)
— Reader<T super Poem>
— тип T
это родительский тип для Poem
(как его иногда называют, супер-тип, отсюда и синтаксис).
type Author<T extends Text> {
T create_masterpiece();
}
type Reader<T super Text> {
read_out(T some_text)
}
Когда у нас есть такие типы, мы можем их безопасно использовать:
// Пушкин пишет стихи
Author<Poem> pushkin;
// и даже если б он не писал
// ничего кроме, всё равно
// был бы писателем
Author<Text> writer = pushkin;
// Книжный червь читает всё
Reader<Text> bookworm;
// и стихи тоже
Reader<Poem> poetry_fan = bookworm;
А вот наша табличка ко-ко-контравариантности:
ковариантное наследование
Author<T extends Text> ← Author<Text>
T extends Text ← Text
например Poem
или Article
——————————————————————————————————————————————
контравариантное наследование
Reader<Poem> → Reader<T super Poem>
Poem ← T super Poem
например Text
или Object
———
Если вы к этому месту окончательно запутались, вы в хорошей компании. В этих super
и extend
я тоже запутался. И неоднократно!
Поэтому мне очень понравилось, как то же самое элегантно решено в Kotlin (и, говорят, ещё раньше в C#)!
По умолчанию обобщённые типы всё так же инвариантны. А для ко-/контра-вариантности там сделано по-уму: у параметра типа можно поставить один из двух модификаторов: in
или out
.
Модификатор in
говорит, что объекты типа T
употребляются только в качестве аргумента функций. То есть приходят в наши функции на вход. Функции являются потребителями (читателями) объектов типа T
.
Модификатор out
обозначает, что объекты типа T
употребляется только в качестве возвращаемого значения функций. Иными словами, функции являются производителями (авторами) объектов типа T
.
То есть in
идёт функции на вход, out
— на выход:
out in
↓ ↓
T action(T argument, ...)
Давайте на примере, там всё становится ещё проще.
type Author<out T: Text> {
T create_masterpiece();
}
type Reader<in T> {
read_out(T some_text)
}
Есть ли разница, писать in/out
или super/extends
? На мой взгляд, отличия весьма существенные.
Слова super/extends
описывают отношение между типами. А мы видели, что типы ведут себя не слишком-то интуитивно: множество стихотворений не является множеством текстов.
В действительности вам нужно не оно, вам требуется знать, можно или нельзя использовать объект типа T
в указанной роли. И ключевые слова in/out
говорят ровно об этом.
Эти слова скрывают от вас сложности наследования обобщённых типов, делают явным то, какие операции разрешёны, а какие запрещёны, и уменьшают когнитивную нагрузку. Вы можете даже не знать, как это работает под капотом, достаточно понимать, что здесь вы поэму вместо текста вставить можете, а там — не можете.
Это как направляющие «ключи» на деталях, не позволяющие их вставить в прибор неправильной стороной.
Возможность меньше задумываться о деталях и больше о существенном — огромное достоинство хорошей нотации.
В начало (↑)
(4/4)