(←) предыдущая запись ; следующая запись (→)
Предположим, как мы сказали ранее, что множество стихотворений является множеством текстов: 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)