До сих пор не можете спать, пытаясь осмыслить понятия ковариантности и контравариантности? Чувствуете, как они дышат вам в спину, но когда оборачиваетесь ничего не находите? Есть решение!  Меня зовут Никита, и сегодня мы попытаемся заставить механизм в голове работать корректно. Вас ожидает максимально доступное рассмотрение темы вариантности в примерах. Добро пожаловать под кат. Вариантность в данном посте разбирается безотносительно к какому-либо языку программирования. Примеры в разделе практики написаны на псевдоязыке (он чудом оказался похож на C#) и поэтому не обязаны компилироваться вашим любимым компилятором. Приступим. В документации, технической литературе и других источниках вы могли встречаться с различными названиями для явлений вариантности. Больше не стоит пугаться и путаться. Термины ковариантность и ковариация эквивалентны (по крайней мере в программировании). Более того, термины контравариантность и контравариация также эквивалентны. Так, например, термины ковариантность и контравариантность используется в Википедии и у Троелсена (в переводе). А термины ковариация и контравариация встречаются, например, на MSDN и у Скита (в переводе). В английском языке всё проще — covariance и contravariance. Вариантность — перенос наследования исходных типов на производные от них типы. Под производными типами понимаются контейнеры, делегаты, обобщения, а не типы, связанные отношениями "предок-потомок". Различными видами вариантности являются ковариантность, контравариантность и инвариантность. Ковариантность — перенос наследования исходных типов на производные от них типы в прямом порядке.
Контравариантность — перенос наследования исходных типов на производные от них типы в обратном порядке.
Инвариантность — ситуация, когда наследование исходных типов не переносится на производные. Если у производных типов наблюдается ковариантность, говорят, что они ковариантны исходному типу. Если у производных типов наблюдается контравариантность, говорят, что они контравариантны исходному типу. Если у производных типов не наблюдается ни того, ни другого, говорят, что они инвариантны.  Вот и всё, что нужно знать. Конечно, тем кто первый раз сталкивается с вариантностью, трудно вникнуть. Поэтому рассмотрим конкретные примеры.  Вся суть вариантности состоит в использовании в производных типах преимуществ наследования. Известно, что если два типа связаны отношением "предок-потомок", то объект потомка может храниться в переменной типа предка. На практике это значит, что мы можем использовать для каких-либо операций объекты потомка вместо объектов предка. Тем самым, можно писать более гибкий и короткий код для выполнения действий поддерживаемых разными потомками с общим предком. Для начала опишем иерархию типов, которой будем оперировать. Вверху иерархии у нас находится Device (устройство), потомками которого являются Mouse (мышь), Keyboard (клавиатура). У Mouse в свою очередь тоже есть потомки — WiredMouse (проводная мышь), WirelessMouse (беспроводная мышь).  Все любят контейнеры. На их примере наиболее просто объяснить, что подразумевается под производными типами. Если говорить о списках как производных типах, то для типа Device производным будет
List<Device> (список устройств). Аналогично, для типа Keyboard производным будет List<Keyboard> (список клавиатур). Думаю, если и были сомнения, то теперь их нет. Ковариантность также легче изучать на примере контейнеров. Для этого выделим часть иерархии (ветвь) — Keyboard : Device (клавиатура является устройством, клавиатура частный случай устройства). Опять возьмём списки и построим ковариантную производную ветвь — List<Keyboard> : List<Device> (список клавиатур является частным случаем списка устройств). Как видим, наследование передалось в прямом порядке. Рассмотрим пример кода. Есть функция, которая принимает список устройств List<Device> и совершает над ними какие-то манипуляции. Как вы уже догадались, в эту функцию можно передать список клавиатур List<Keyboard>: Каноническим для изучения контравариантности является рассмотрение её на основе делегатов. Допустим, у нас есть обобщённый делегат: Для исходного типа Device производным будет Action<Device>, а для Keyboard — Action<Keyboard>. Полученные делегаты могут представлять функции, которые выполняют какие-то действия над устройством или мышью соответственно. Для ветви Keyboard : Device построим производную контравариантную ветвь — Action<Device> : Action<Keyboard> (действие над устройством является частным случаем действия над клавиатурой — звучит странно, но так и есть). Если можно нажать клавишу на клавиатуре, то это не значит, что и на устройстве можно нажать её (оно может не иметь понятия о том, что такое клавиша). Но если можно подключить устройство, то можно этим же способом (методом, функцией) подключить и клавиатуру. Как видим, наследование передалось в обратном порядке. Из выше сказанного логично, что если функция может выполнить, что-то над устройством, то она может выполнить это и над клавиатурой. Это значит, мы можем передать объект делегата Action<Device> в функцию, принимающую объект делегата Action<Keyboard>. Рассмотрим в коде: Если производные типы инвариантны к исходным типам, то для ветви Keyboard : Device не образуется ни ковариантной (List<Keyboard> : List<Device>), ни контравариантной (Action<Device> : Action<Keyboard>) ветви. Это значит, что нет никакой связи между производными типами. Как видим, наследование не переносится. Делегаты типа Action<T> могут быть ковариантны. Это значит, что для ветви Keyboard : Device образуется ковариантная ветвь — Action<Keyboard> : Action<Device>. Таким образом, в функцию, принимающую объект делегата Action<Device>, можно передавать объект делегата Action<Keyboard>. Контейнеры могут быть контравариантны. Это значит, что для ветви Keyboard : Device образуется контравариантная ветвь — List<Device> : List<Keyboard>. Таким образом, в функцию, принимающую List<Keyboard>, можно передавать List<Device>: Рассмотренные выше экзотические виды вариантности имеют, разве что, академическую ценность. Сложно придумать реальную задачу, которая легче решается при наличии такого рода возможностей. Стоит запомнить, что ковариантность и контравариантность могут вызывать ошибки времени выполнения. Для их устранения требуется вводить определённые ограничения. Компиляторы, как правило, такие ограничения не вводят. Если производный тип ковариантен, то для обеспечения безопасности контейнер должен быть read only. В противном случае, остаётся возможность записать в List<Keyboard> объект неверного типа (Device, Mouse и другие) через приведение к List<Device>: Если производный тип контравариантен, то для обеспечения безопасности контейнер должен быть write only. В противном случае, остаётся возможность считывания из List<Device> объекта неверного типа (Keyboard, Mouse и других) через приведение к соответствующему списку (List<Keyboard>, List<Mouse> и другим): Разумным для делегатов является ковариантность для выходного значения и контравариантность для входных параметров (исключая передачу по ссылке). В случае соблюдения данных условий ошибок времени выполнения не возникает. Представленных примеров достаточно для понимания принципов работы вариантности. Данные о её поддержке разными типами вашего любимого языка ищите в соответствующей спецификации. Если что-то пошло не так — закройте глаза, выдохните и выпейте чай. После этого попытайтесь снова. Спасибо за внимание. Возможно более правильным определением вариантности является предложенное Эриком Липпертом. Спасибо Alex_sik за ссылку на статью. Совместимость присваивания, assignment compatibility — это возможность присвоить значение более частного типа совместимой переменной более общего типа.
Вариантность — это сохранение совместимости присваивания исходных типов у производных типов.
Ковариантность — это сохранение совместимости присваивания исходных типов у производных в прямом порядке.
Контравариантность — это сохранение совместимости присваивания исходных типов у производных в обратном порядке.