Различают ковариацию и контравариацию .
Грубо говоря, операция является ковариантной, если она сохраняет порядок типов, и контрвариантной, если она переворачивает этот порядок.
Сам порядок должен представлять более общие типы как более крупные, чем более конкретные типы.
Вот один пример ситуации, когда 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);
A 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, поддержка отклонений в обобщениях была очищена, и теперь можно использовать новые ключевые слова в и out с параметрами общего типа. Они могут определять и ограничивать направление потока данных для определенного параметра типа, позволяя работать дисперсии. Но в случае List<T>
данные типа T
передаются в обоих направлениях - существуют методы типа List<T>
, которые возвращают T
значения, и другие, которые получают такие значения.
Смысл этих направленных ограничений - , чтобы разрешить дисперсию там, где это имеет смысл , но предотвратить проблемы , такие как ошибка времени выполнения, упомянутая в одном из предыдущих примеров массива. Если параметры типа правильно оформлены с помощью в или из , компилятор может проверить и разрешить или запретить его отклонение в время компиляции , 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 для параметра типа T
.
При работе с универсальными типами важно учитывать дисперсию и то, как компилятор применяет различные виды хитрости, чтобы заставить ваш код работать так, как вы ожидаете.
Существует больше информации о дисперсии, чем описано в этой главе, но этого будет достаточно, чтобы сделать весь следующий код понятным.
Ссылка: