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

Предположим, как мы сказали ранее, что множество стихотворений является множеством текстов: Set<Poem> ← Set<Text>.

Наш список стихотворений Set<Poem> имеет метод get, возвращающий Poem. А если мы будем трактовать список стихотворений как список текстов (мы только что предположили, что делать так разрешено), то метод get вернёт просто Text. Вроде нормально: стихотворение является текстом.

Кроме этого Set<Poem> имеет метод put, добавляющий Poem в список. Но! Если мы снова трактуем наш объект как список текстов, то метод put имеет возможность принять любой Text и добавить его в список. Остановитесь на секунду и осознайте проблему.

Получается, что мы можем добавить в список стихотворений произвольный текст, например текст научной статьи. Так быть не должно.
Хуже того, потом мы можем достать этот текст и трактовать его как стихотворение.

Посочувствуйте нашему чтецу, который вынужден будет художественно декламировать статью по химии.

Я приведу псевдокод, который иллюстрирует проблему.
Если вы рыбка и вам такое сложновато, то feel free to skip.
Но там есть стих из статьи. Я старался!

Set<Poem> set_of_poems; // список стихов
 
// Переинтерпретируем его как список просто текстов.
// Это не другой список, а тот же самый, на который
//   мы смотрим под другими углом
// Мы имеем право так делать, потому что список
//   стихотворений считаем списком текстов
Set<Text> set_interpreted_as_texts = set_of_poems;
 
// Добавили статью в переинтерпретированный список стихов.
// Теперь у нас в списке стихов есть непоэтический текст.
// Проблема!!!
set_interpreted_as_texts.put(article_1);
 
// На самом деле, благодаря принципу подстановки Лисков,
// мы могли бы добавить её и напрямую.
// Результат был бы тот же — ошибка
set_of_poems.put(article_2);
 
// Методом get мы получаем научную статью.
// Имеем право: set_interpreted_as_texts — список текстов
Text text_1 = set_interpreted_as_texts.get();
 
Reader<Text> universal_reader;  // чтец-универсал
// Он может зачитать написанное в статье
reader.read_out(text_1);
 
    // H2 + 2⋅O2 → 2H2O + Energy
 
// Теперь вызываем get у исходного списка (поэтического).
// Из множества может быть выбрана научная статья,
// хотя set_of_poems гарантировал, что операция get
//   нам выдаст стихотворение
//
// Мы попытались интерпретировать статью как стих,
//   но вообще-то это некорректная операция!!!
Poem poem_2 = set_of_poems.get();
 
// И наконец прочтём поэму о химии.
Reader<Poem> poem_reader;   // чтец поэзии
// Пытается зачитать статью по химии
poem_reader.read_out(poem_2);
 
    // В реторту засыпь пару моль водорода,
    // Кислорода добавь соразмерно,
    // Чиркни спичкой,
    // Adios.

Шутки шутками, но вообще-то мы получили нестыкуемые типы. Мы не имеем права так делать. Получается, что Set<Poem> не является Set<Text>.

———

Но может быть всё наоборот, и это Set<Text> является Set<Poem>?

«Множество текстов является множеством стихов».
Даже звучит странно, но давайте явно удостоверимся, что это глупость, и выпишем пример, на котором такая трактовка наследования сломается:

// Заведём набор текстов
Set<Text> texts;
 
// переинтерпретируем его как набор стихов
Set<Poem> reinterpreted_set = texts;
 
// Функция get для типа Set<Poem> должна выдавать
//   объекты типа «стихотворение».
// Но вообще-то там хранятся тексты общего вида.
// Снова проблема!
Poem poem_1 = reinterpreted_set.get();

Снова нет! В обратную сторону это тоже не работает и Set<Text> не является Set<Poem>.

———

Итак, наследование не ковариантно и не контравариантно по типу параметра.
Типы «набор текстов» и «набор стихов» независимы и никак друг с другом не соотносятся. Set<Text> и Set<Poem> нельзя использовать взаимозаменяемо.
Такое наследование называется инвариантным.

Мы теперь можем расширить нашу таблицу:

       Poem  ←        Text   базовый тип
Author<Poem> ← Author<Text>  ковариантное наследование
Reader<Poem> → Reader<Text>  контравариантное наследование
   Set<Poem> ≠    Set<Text>  инвариантное наследование

(3/4)