В чем разница между ковариантностью и совместимостью по присваиванию?

Я об этом уже неоднократно писал, но думаю, что один момент требует повторения.

По мере приближения к выпуску C# 4.0, мне стали попадаться множество документов, статей в блогах и т.п., в которых делается попытка объяснить значения термина «ковариантность» (covariant). Очень сложно дать понятное определение этого термина для людей не знакомых с теорией категорий, но сделать это можно. И я думаю, важно избегать определения термина, которое бы отличалось от его истинного значения.

Множество документов содержало что-то в таком духе:

«Ковариантность – это способность присвоить выражение более конкретного типа переменной менее конкретного типа. Например, рассмотрим метод M, который возвращает экземпляр класса Giraffe (жираф). Вы можете присвоить результат выполнения M переменной типа Animal (животное), поскольку Animal менее конкретный тип, совместимый с типом Giraffe. Методы в C# «ковариантны» по типу возвращаемого значения, поэтому при создании ковариантного интерфейса это указывается с помощью ключевого слова «out» – возвращаемое значение возвращается (comes out) из метода.»

Но это определение не имеет никакого отношения к понятию ковариантности. Это описание «совместимости по присваиванию» (assignment compatibility). Способность присвоить значение более конкретного типа для хранения в переменной совместимого менее конкретного типа называется «совместимостью по присваиванию», поскольку эти два типа являются совместимыми (compatible) в контексте проверки корректности присваивания. В таком случае, что же означает понятие ковариантности?

Прежде всего мы должны четко определить, к чему применяется прилагательное «ковариантный». Здесь я хочу прибегнуть к несколько более формальному определению, но постараюсь оставаться понятным. Давайте начнем даже не с рассмотрения типов; давайте подумаем насчет целых чисел. (Здесь я говорю о настоящих математических целых числах, а не о непонятном поведении 32- разрядных целых чисел в контексте отсутствия проверок переполнения.) Точнее, мы собираемся рассматривать отношения «меньше или равно» целых чисел. (Вспомните, что «отношение» – это функция, которая принимает два аргумента и возвращает булев результат, который показывает, выполняется это отношение или нет).

Давайте рассмотрим проекцию целых чисел. Что такое проекция? Проекция – это функция, принимающая целое число и возвращающая другое целое число. Так, например, z → z + z является проекцией; назовем ее D от «double» (удвоение). Так, проекцию z → 0 – z, назовем N от «negate» (отрицание), а проекцию z → z * z, назовем S от «square» (возведение в квадрат).

А теперь, интересный вопрос. Всегда ли верно условие: (x ≤ y) = (D(x) ≤ D(y))? Да, всегда. Если x меньше y, значит удвоенное значение x меньше удвоенного значения y. Если x равен y, тогда удвоенное значение x равно удвоенному значению y. Если x больше y, тогда удвоенное значение x больше удвоенного значения y. Проекция D сохраняет направление отношения по значению. А как насчет проекции N? Всегда ли верно (x ≤ y) = (N(x) ≤ N(y))? Конечно же нет. Условие 1 ≤ 2 истинно,но -1 ≤ -2 – ложно. Но можно заметить, что противоположное условие всегда истинно! (x ≤ y) = (N(y) ≤ N(x)). Проекция N изменяет на противоположное направление отношения по значению.

А как насчет S? Всегда ли верно (x ≤ y) = (S(x) ≤ S(y))? Нет. Условие -1 ≤ 0 истинно, но S(-1) ≤ S(0) – ложно. А как насчет противоположных условий? Всегда ли верно (x ≤ y) = (S(y) ≤ S(x))? Опять таки, нет. Условие 1 ≤ 2 истинно, а S(2) ≤ S(1) – ложно. Проекция S не сохраняет направление отношения по значению, но и не меняет его на противоположное.

Проекция D является «ковариантной», поскольку она сохраняет отношение порядка целых чисел. Проекция N является «контрвариантной». Она изменяет отношение порядка целых чисел на противоположное. Проекция S не является ни той, ни другой, она «инвариантна».

Я надеюсь теперь стало понятнее, что же такое ковариантность и контрвариантность. Целые числа сами по себе неизменны, как и отношение «меньше» между ними. Это проекция может является ковариантной или контрвариантной – правило по которому старое значение целого числа превращает в новое.

Теперь давайте оставим целые числа и подумаем о ссылочных типах (reference types). Вместо отношения ≤ для целых чисел у нас есть отношение ≤ для ссылочных типов. Ссылочный тип X меньше чем (или равен) ссылочному типу Y если значение типа X может быть сохранено в переменной типа Y, т.е., если X «совместим по присваиванию» с Y. Давайте теперь рассмотрим проекцию одних типов к другим. Скажем, проекцию “T переходит в IEnumerable<T>”. Т.е. у нас есть проекция, принимающая тип, скажем, Giraffe, и возвращая новый тип, IEnumerable<Giraffe>. Является ли эта проекция ковариантной в C# 4? Она сохраняет направление отношения порядка (ordering). Объект класса Giraffe может быть присвоен переменной типа Animal, поэтому последовательность объектов класса Giraffe может быть присвоен переменной, предназначенной для хранения последовательности объектов класса Animal. Мы можем рассматривать обобщенные типы как «образцы» (blueprints) для создания типов. Давайте рассмотрим проекцию, которая принимает T и возвращает IEnumerable<T> и назовем эту проекцию просто “IEnumerable<T>”. Из контекста мы можем понять, что когда мы говорим, что «проекция IEnumerable<T> является ковариантной», мы имеем ввиду следующее: «проекция, которая принимает ссылочный тип T и возвращает ссылочный тип IEnumerable<T> является ковариантной». А поскольку IEnumerable<T> содержит только один параметр типа, из контекста ясно, что параметром проекции является тип T. Кроме того, гораздо проще сказать “проекция IEnumerable<T> ковариантна», чем ту сложнопроизносимую фразу.

Итак, теперь мы можем определить понятия ковариантности, контрвариантности и инвариантности. Обобщенный тип I<T> является ковариантным (по отношению к T) если конструкция с аргументами ссылочного типа сохраняет направление совместимости по присваиванию. Тип является контрвариантным (по отношению к T) если направление совместимости по присваиванию меняется на противоположное. В противном случае тип является инвариантным. Таким способом, мы просто говорим в краткой форме, что проекция, которая принимает T и возвращает I<T> является ковариантной/контрвариантной/инвариантной проекцией.

UPDATE: Мой близкий друг (и компьютерный гик) Джен (Jen) заметила, что в серии романов «Сумерки» (Twilight), так называемые оборотни (которые не превращаются в волков при полной луне, поэтому на самом деле они оборотнями не являются) сохраняют жесткий социальный порядок в обоих личинах, как волка, так и человека. Проекция от человека к волку является ковариантной в социальном отношении. Она также заметила, что в старших классах гики в языках программирования находятся в самом низу социальной лестницы, но проекция взросления перебрасывает их наверх, поэтому можно считать, что взросление является контравариантным. Я не совсем уверен во втором высказывании, но за первое могу ручаться. Я думаю, что вопрос о том, как работает социальный порядок для подростков-оборотней, которые являются компьютерными гиками, требует дополнительных исследований. Спасибо, Джен!

Оригинальное сообщение: What's the difference between covariance and assignment compatibility?