(←) предыдущая запись ; следующая запись (→)

Теперь вернёмся к вопросу про нотацию и именование сущностей.

Мы разобрали три типа наследования параметризованных типов:
— ковариантное (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)