Ковариантность и контравариантность в языках программирования
Может кто-нибудь объяснить мне, концепция ковариации и контравариантности в теории языков программирования?
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
Для дополнительного удобства, вот упорядоченный список ссылок на все статьи Эрика Липперта о дисперсии:
- Ковариантность и контравариантность в C#, часть первая
- Ковариантность и контравариантность в C#, часть вторая: ковариация массива
- Ковариантность и контравариантность в C#, часть третья: дисперсия преобразования группы методов
- Ковариантность и контравариантность в C#, часть четвертая: реальная дисперсия делегата
- Ковариантность и контравариантность в C#, часть пятая: функции высшего порядка повредили мой мозг
- Ковариантность и Контравариантность в C#, Часть шестая: Дисперсия интерфейса
- Ковариантность и контравариантность в C# Часть седьмая: зачем вообще нужен синтаксис?
- Ковариантность и контравариантность в C#, часть восьмая: параметры синтаксиса
- Ковариантность и контравариантность в C#, часть девятая: переломные изменения
- Ковариантность и контравариантность в 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
Барт Де Смет имеет отличную запись в блоге о ковариации и контравариантности здесь.
И 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# выдаст ошибку, если вы попытаетесь сделать что-то, что не поддерживается.