Понятие дисперсии вступает в игру, когда мы рассматриваем классы с параметрами типа, то есть дженерики.

Для классов T и T’, где T’ является подклассом T, какова связь между A[T] и A[T’]?

Или, немного менее абстрактно. Какая связь между List[Animal] и List[Cat], учитывая, что Cat является подклассом Animal?

Ковариация

Интуитивно понятно, что List[Cat] должен быть подклассом List[Animal]. Всякий раз, когда иерархия наследования класса A[T] отражает иерархию класса T, мы говорим, что A является ковариантным по своему аргументу типа Т. Будучи ковариантным, мы можем выполнить следующее задание для животных:

var cats: List[Cat] = new List()[Cat] 
var animals: List[Animal] = cats

Но он также позволяет вызывать animals.add(new Dog()). Ни присвоение, ни добавление собаки к животным незаконны — компилятор не будет жаловаться. Однако мы знаем, что животные — это замаскированный List[Cat], и ничего хорошего из этого не выйдет.

Ковариация интуитивно понятна, да, но опасна, когда речь идет о изменчивости. Вот как некоторые языки программирования, с которыми я работал, справляются с этим:

  • В Swift коллекции ковариантны. Это безопасно, потому что они семантически неизменны.
  • Java немного запутанный. Массивы были частью языка до того, как были добавлены дженерики. Они ковариантны и будут падать во время выполнения в приведенном выше примере. Однако все универсальные типы инвариантны, независимо от изменчивости.
  • Scala улучшила решение Java. Неизменяемые списки ковариантны, а изменяемые списки инвариантны. Вы можете выбрать дисперсию для пользовательских типов, и компилятор поможет выбрать правильный.

Контравариантность

Контравариантность немного не интуитивно понятна, но она действительна. Тип A является контравариантным, когда A[T] является подклассом A[T'] при условии, что T' является подклассом T. Это противоположность ковариации.

Это актуально только для языков, которые поддерживают функции первого класса:

val animalFunc: (Animal) => String = 
  (animal) => { animal.toString() }

Здесь у нас есть функция animalFunc, которая берет Animal и возвращает String. Оказывается, функции контравариантны по своим аргументам. Мы могли бы назначить animalFunc везде, где мы ожидаем, что функция будет принимать подкласс Animal в качестве параметра:

val catFn: (Cat) => String = animalFunc

Обратите внимание, что здесь происходит: мы назначаем (Animal) =› String (Cat) =› String. catFn ожидает функцию, которая принимает Cat, а мы передаем функцию, которая принимает Animal. Это возможно, потому что аргументы функций контравариантны: Animal =› String является подклассом Cat =› String, обратная связь между Cat и Животное.

Небольшой экскурс в Scala…

Функции Scala на самом деле являются экземплярами класса. Подпись типа (Animal) => String — это просто синтаксический сахар для Function1[-Animal, +String]. Знак - означает контравариантность, а + означает ковариантность.

На простом английском языке Function1[-Animal, +String] переводится как функция, которая принимает один контравариантный аргумент типа Animal и возвращает ковариантный результат типа String. сильный>.

Почему контравариантность?

Легче понять контравариантность на контрпримере. Вместо этого представьте, что функции были ковариантны по своим аргументам. Если бы это было так, мы могли бы присвоить catFn animalFn ниже.

val catFn: (Cat) => String = (cat) => { cat.meow() } 
val animalFn: (Animal) => String = catFunc 
animalFn(new Dog()) // Crashes when trying to call meow()

Поскольку animalFn объявлен как Animal =› String, он принимает любое животное, поэтому мы передаем ему новый Dog(). Как и в примере со списком для конвариантности, animalFn на самом деле указывает на что-то другое: в данном случае на catFn. На самом деле мы вызываем catFn(new Dog()), что приведет к сбою, потому что все знают, что собаки не реализуют мяуканье.

Все дело в позиции

Списки не ковариантны, потому что они списки. В более общем случае для типа A[T] дисперсия не является свойством A. Вместо этого он получен из того, как T используется внутри A.

Это лучше с примерами:

  • Функция T =› R контравариантна на T, потому что аргументы метода находятся в контравариантной позиции. Это относится и к пользовательским типам: если вы определяете класс A[T] с помощью методов foo(arg: T), то A > является контравариантным на T.
  • Неизменяемый List[T] является ковариантным, потому что при его реализации T всегда появляется либо как возвращаемый тип, либо как неизменяемая переменная: обе ковариантные позиции.
  • Изменяемый списокList[T], скорее всего, будет иметь методы add(elem: T) и get(index: Int):T. Здесь T появляется как в ковариантной (возвращаемый тип при получении), так и в контравариантной (аргумент метода при добавлении) позициях. Всякий раз, когда возникает подобный конфликт, класс должен быть инвариантным относительно типа T. Вы также можете выбрать класс как инвариантный, даже если он может быть ковариантным или контравариантным, но по какой-то причине вам не нужна никакая дисперсия.

Подведение итогов

Для типов T и T’, где T’ является подклассом T, класс A[U]:

  • ковариантный, когда A[T’] является подклассом A[T]
  • контравариантно, когда A[T] является подклассом A[T’]
  • инвариантно, когда A[T] и A[T’] не связаны друг с другом. Они такие же разные, как Кошка и Собака.

Некоторые другие статьи

В Википедии есть хорошее объяснение с примерами на Java и C#. Они говорят и о проблеме Java с ковариантностью массивов.

Александрос Салазар написал отличную статью о дисперсии типов Swift.

Первоначально опубликовано на linorosa.com.