Ковариантность и контравариантность в языках программирования

Может кто-нибудь объяснить мне, концепция ковариации и контравариантности в теории языков программирования?

8 ответов

Решение

Ковариантность довольно проста и о ней лучше всего думать с точки зрения некоторого коллекционного класса List, Мы можем параметризовать List класс с некоторым параметром типа T, То есть наш список содержит элементы типа T для некоторых T, Список будет ковариантным, если

S является подтипом T iff List[S] является подтипом List [T]

(Где я использую математическое определение, если оно имеет в виду, если и только если.)

Это List[Apple] это List[Fruit], Если есть какая-то рутина, которая принимает List[Fruit] в качестве параметра, и у меня есть List[Apple]тогда я могу передать это в качестве допустимого параметра.

def something(l: List[Fruit]) {
    l.add(new Pear())
}

Если наш класс коллекции List является изменчивым, то ковариация не имеет смысла, потому что мы можем предположить, что наша рутина может добавить какой-то другой фрукт (который не был яблоком), как указано выше. Следовательно, нам нужно только, чтобы неизменяемые классы коллекций были ковариантными!

Различают ковариацию и контравариантность.
Грубо говоря, операция является ковариантной, если она сохраняет порядок типов, и контрвариантной, если она инвертирует этот порядок.

Сам порядок должен представлять более общие типы как более крупные, чем более конкретные типы.
Вот один пример ситуации, когда C# поддерживает ковариацию. Во-первых, это массив объектов:

object[] objects=new object[3];
objects[0]=new object();
objects[1]="Just a string";
objects[2]=10;

Конечно, в массив можно вставить разные значения, потому что в итоге все они получаются из System.Object в.Net Framework. Другими словами, System.Object это очень общий или большой тип. Теперь вот место, где поддерживается ковариация:
присваивание значения меньшего типа переменной большего типа

string[] strings=new string[] { "one", "two", "three" };
objects=strings;

Переменные объекты, которые имеют тип object[], может хранить значение, которое на самом деле типа string[],

Подумайте об этом - до некоторой степени, это то, что вы ожидаете, но с другой стороны это не так. Ведь пока string происходит от object, string[] НЕ вытекает из object[], Языковая поддержка ковариации в этом примере делает назначение в любом случае возможным, что вы найдете во многих случаях. Дисперсия - это функция, которая делает язык более интуитивно понятным.

Соображения вокруг этих тем чрезвычайно сложны. Например, на основе предыдущего кода, здесь есть два сценария, которые приведут к ошибкам.

// Runtime exception here - the array is still of type string[],
// ints can't be inserted
objects[2]=10;

// Compiler error here - covariance support in this scenario only
// covers reference types, and int is a value type
int[] ints=new int[] { 1, 2, 3 };
objects=ints;

Пример работы контравариантности немного сложнее. Представьте себе эти два класса:

public partial class Person: IPerson {
    public Person() {
    }
}

public partial class Woman: Person {
    public Woman() {
    }
}

Woman происходит от Person очевидно. Теперь рассмотрим, у вас есть эти две функции:

static void WorkWithPerson(Person person) {
}

static void WorkWithWoman(Woman woman) {
}

Одна из функций делает что-то (неважно, что) с Woman другой является более общим и может работать с любым типом, полученным из Person, На Woman сторона вещей, теперь у вас также есть эти:

delegate void AcceptWomanDelegate(Woman person);

static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
    acceptWoman(woman);
}

DoWork это функция, которая может взять Woman и ссылка на функцию, которая также принимает Woman и затем он передает экземпляр Woman делегату. Рассмотрим полиморфизм элементов у вас здесь. Person больше чем Woman, а также WorkWithPerson больше чем WorkWithWoman, WorkWithPerson также считается больше, чем AcceptWomanDelegate с целью отклонения.

Наконец, у вас есть эти три строки кода:

Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);

Woman Экземпляр создан. Затем вызывается DoWork, передавая в Woman экземпляр, а также ссылка на WorkWithWoman метод. Последнее явно совместимо с типом делегата AcceptWomanDelegate - один параметр типа Woman, нет возвращаемого типа. Третья строка немного странная. Метод WorkWithPerson занимает Person в качестве параметра, а не Woman в соответствии с требованиями AcceptWomanDelegate, тем не менее, WorkWithPerson совместим с типом делегата. Контравариантность делает это возможным, поэтому в случае делегатов больший тип WorkWithPerson может храниться в переменной меньшего типа AcceptWomanDelegate, Еще раз это интуитивная вещь: если WorkWithPerson может работать с любым Person проходя в Woman не могу ошибаться, верно?

К настоящему времени вам может быть интересно, как все это относится к генерикам. Ответ в том, что дисперсия может применяться и к генерикам. Предыдущий пример используется object а также string массивы. Здесь код использует общие списки вместо массивов:

List<object> objectList=new List<object>();
List<string> stringList=new List<string>();
objectList=stringList;

Если вы попробуете это, вы обнаружите, что это не поддерживаемый сценарий в C#. В C# версии 4.0, а также.Net Framework 4.0, поддержка дисперсии в обобщениях была очищена, и теперь можно использовать новые ключевые слова с параметрами универсального типа. Они могут определять и ограничивать направление потока данных для определенного параметра типа, позволяя работать с отклонениями. Но в случае List<T> данные типа T течет в обоих направлениях - есть методы по типу List<T> это возвращение T ценности и другие, которые получают такие значения.

Смысл этих направленных ограничений состоит в том, чтобы разрешить дисперсию там, где это имеет смысл, но предотвратить такие проблемы, как ошибка времени выполнения, упомянутая в одном из предыдущих примеров массива. Когда параметры типа правильно декорированы с помощью in или out, компилятор может проверить и разрешить или запретить его отклонение во время компиляции. Microsoft приложила усилия для добавления этих ключевых слов во многие стандартные интерфейсы в среде.Net, например IEnumerable<T>:

public interface IEnumerable<out T>: IEnumerable {
    // ...
}

Для этого интерфейса поток данных типа T Объекты понятны: они могут быть извлечены только из методов, поддерживаемых этим интерфейсом, но не переданы в них. В результате можно построить пример, аналогичный List<T> попытка описана ранее, но с использованием IEnumerable<T>:

IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;

Этот код приемлем для компилятора C# начиная с версии 4.0, потому что IEnumerable<T> является ковариантным из- за спецификатора out в параметре type T,

При работе с универсальными типами важно учитывать дисперсию и то, как компилятор применяет различные виды хитрости, чтобы заставить ваш код работать так, как вы ожидаете.

Существует больше информации о дисперсии, чем описано в этой главе, но этого будет достаточно, чтобы сделать весь следующий код понятным.

Ref:

  • PROFESSIONAL Functional Programming in C#

Вот мои статьи о том, как мы добавили новые функции дисперсии в C# 4.0. Начните снизу.

http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx

Для дополнительного удобства, вот упорядоченный список ссылок на все статьи Эрика Липперта о дисперсии:

  1. Ковариантность и контравариантность в C#, часть первая
  2. Ковариантность и контравариантность в C#, часть вторая: ковариация массива
  3. Ковариантность и контравариантность в C#, часть третья: дисперсия преобразования группы методов
  4. Ковариантность и контравариантность в C#, часть четвертая: реальная дисперсия делегата
  5. Ковариантность и контравариантность в C#, часть пятая: функции высшего порядка повредили мой мозг
  6. Ковариантность и Контравариантность в C#, Часть шестая: Дисперсия интерфейса
  7. Ковариантность и контравариантность в C# Часть седьмая: зачем вообще нужен синтаксис?
  8. Ковариантность и контравариантность в C#, часть восьмая: параметры синтаксиса
  9. Ковариантность и контравариантность в C#, часть девятая: переломные изменения
  10. Ковариантность и контравариантность в C#, часть десятая: работа с двусмысленностью

Тип (T)

composite data type- тип, построенный из другого типа. Например, это могут быть универсальные шаблоны с подстановочными знаками, контейнеры, типы функций... [пример]

method's types - возвращаемое значение и значение параметров

Variance- о совместимости назначений. Это способность использоватьderived type вместо того original type. Это не parent-child отношения

Х (Т) - composite data type или method's types X, с типом T

Covarianceвы можете назначить больше derived type чем original type
X(T) ковариантно или X(T1) ковариантно X(T2), когда отношение T1 к T2 такое же, как отношение X(T1) к X(T2)

Contravarianceты можешь назначать меньше derived type тогда original type
X(T) контравариантно или X(T1) контравариантно X(T2), когда отношение T1 к T2 такое же, как отношение X(T2) к X(T1)

Invariance ни то, ни другое Covariance не Contravariance

Примеры

class A { }

//B is A
class B extends A { }

Ссылочный тип Array в Java ковариантен

A[] aArray = new A[2];
B[] bArray = new B[2];

//B[] is covariant to A[] because
aArray = bArray;
class Generic<T> { }

//A - original type, B - more derived type
//Generic<B> is covariant to Generic<A>
Generic<? extends A> ref = new Generic<B>(); //covariant

//B - original type, A - less derived type
//Generic<B> is contravariant to Generic<A>
Generic<? super B> ref = new Generic<A>(); //contravariant

[Дженерики Java]

Барт Де Смет имеет отличную запись в блоге о ковариации и контравариантности здесь.

И C#, и CLR допускают ковариацию и противоречивость ссылочных типов при привязке метода к делегату. Ковариантность означает, что метод может возвращать тип, производный от возвращаемого типа делегата. Контрастность означает, что метод может принимать параметр, который является основой типа параметра делегата. Например, с учетом делегата, определенного следующим образом:

делегировать объект MyCallback(FileStream s);

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

как это:

String SomeMethod (Stream s);

Здесь возвращаемый тип SomeMethod (String) - это тип, полученный из возвращаемого типа делегата (Object); эта ковариация разрешена. Тип параметра SomeMethod (Stream) - это тип, который является базовым классом типа параметра делегата (FileStream); это противоречие разрешено.

Обратите внимание, что ковариация и контрастность поддерживаются только для ссылочных типов, а не для типов значений или для void. Так, например, я не могу привязать следующий метод к делегату MyCallback:

Int32 SomeOtherMethod (Stream s);

Несмотря на то, что возвращаемый тип SomeOtherMethod (Int32) является производным от возвращаемого типа MyCallback (Object), эта форма ковариации не допускается, поскольку Int32 является типом значения.

Очевидно, что причина, по которой типы значений и void не могут использоваться для ковариации и контравариантности, заключается в том, что структура памяти для этих вещей меняется, тогда как структура памяти для ссылочных типов всегда является указателем. К счастью, компилятор C# выдаст ошибку, если вы попытаетесь сделать что-то, что не поддерживается.

Другие вопросы по тегам