Comments 17
Контравариантность не вписывается в LSP
Ещё как вписывается. Только нужно понимать её правильно, а не так как её понимаете вы.
Ошибка — вот в этой картинке:
Тип "коробка для питомца" не является подтипом для "коробки для животного". Эти типы не являются совместимыми друг с другом ни в одну сторону. Иными словами, понятие "коробка" инвариантно.
Конечно же, вы, как программист, вольны нарушить LSP и обойти это ограничение. Но в таком случае система типов языка перестанет защищать вас от ошибок.
Однако, можно выделить два семейства интерфейсов, наподобие вот таких:
interface TakeOnlyBox<T> {
take() : T;
}
interface PutOnlyBox<T> {
put(item: T);
}
Первый интерфейс образует иерархию подтипов, аналогичную показанной на картинке — то есть TakeOnlyBox<Pet>
является подтипом TakeOnlyBox<Animal>
. Такое семейство типов называется ковариантным.
Второй интерфейс образует иерархию подтипов, "развёрнутую" в обратную сторону: PutOnlyBox<Pet>
является супертипом PutOnlyBox<Animal>
. Такое семейство типов называется контравариантным.
Так вот, если функция pushPet
с картинки будет принимать не просто коробку, а интерфейс PutOnlyBox<Pet>
— это никак не нарушит LSP.
Отношение подтипов и совместимость типов — разные понятия. Фактически лсп утверждает, что второе должно совпадать с первым. Однако, в статье показано, что это не так.
Инвариантность дженериков — это костыль в некоторых языках. Не распространяйте его на всё программиронание. В статье на которую я дал ссылку вы можете найти код на языке D, который умеет правильно выводить вариантность (совместимость). Без глупостей про тотальную инвариантность дженериков.
Если вы считаете, что в "клетку для собак" нельзя сажать других животных, то такую клетку нельзя считать подтипом "клетки для животных". Подтип должен уметь делать всё то же, что и родитель. Например-вмещать в себя кошек.
Комментатор выше то же самое написал, только на чуть другом языке.
P.s. с вашей точки зрения, "коробка для животных с собакой внутри" и "коробка для собак с собакой внутри" — это одно и то же или нет?
А кто вам сказал, что клетка для любого животного является подтипом (ну или супертипом) коробки для домашнего? Вообще говоря, это не определено.
Когда я увидел название, подумал, что будет про Language Server Protocol (протокол, с помощью которого взаимодействуют, например, VScode и Rust Language Server, отделяет редактор от программы, которая генерирует подсказки, проверяет код на ошибки и т.д.).
Поставил плюс за подъём темы в варианте, о котором раньше не приходилось задумываться — как-то просто LSP и вопросы ковариатности/контравариантности не возникали одновременно в одном контексте. Видимо, это просто инерция мышления (LSP обычно формулируется в терминах, соответствующих "входным" сущностям и именно на входе, но "ковариантность" там не звучит).
"Коробка", я понимаю, была аллюзией на коллекцию и/или проблемы вариантности для шаблонных типов (включая коллекции), как раз, два? (Специально взял первые попавшиеся ссылки в поиске.) Мне такое условное представление не очень адекватно, но я его по крайней мере понял.
С другой стороны, тут просто переформулировкой можно исключить ту же проблему. Да, например, вы можете Cat вложить в Box[Cat], а можете в Box[Pet] или Box[Animal]. Но: с точки зрения того, что происходит в пределах формализованного интерфейса/контракта одной функции, как раз LSP не нарушает. Если она возвращает Cat то уже дело принимающего, что он ждёт Pet или Animal. Просто в варианте, когда как, например, в Java/C# мы получаем параметром Box<? super Cat> и вкладываем в него выходное значение, фактически мы рассматриваем тот же LSP, но за счёт специфики оформления интерфейса вместо выходного значения функции мы получаем входное — и для принимающего всё равно выполняется LSP, только для каждого по-своему: кто-то ждал Pet и получил Cat, кто-то ждал Animal и получил Cat.
На "деконструкцию", конечно, не тянет. Псевдо-инверсия (псевдо — потому что на формальном уровне, сохраняя фактический смысл). Но было полезно продумать ещё раз.
Или я какой-то ещё вариант применения упустил?
Деконструкция LSP