Почему параметры ковариантного типа, такие как IEnumerable<out T>
, имеют тип T
, используемый только для типа возвращаемого значения?
Сначала: T
не имеет будет использоваться только для типа возврата.Например:
interface I1<out T>{ T M1(); }
interface I2<out T>{ void M2(Action<I1<T>> a); }
В I1<T>
, T
используется только в позициях возвращаемого типа.Но в I2
, T
используется во входе a
, а I1<T>
является входом Action
, поэтому в некотором смысле он используется в двух позициях вводаздесь.
Но давайте рассмотрим более простой случай.Почему мы можем сделать I1
ковариантным в T
, но не контравариантным в T
?
Причина в том, что ковариация безопасна, а противоположность - нет.Мы можем видеть, что ковариация безопасна:
class Animal {}
class Mammal : Animal {}
class Tiger : Mammal {}
class Giraffe : Mammal {}
class C : I1<Mammal> {
public Mammal M1() { return new Tiger(); }
}
I1<Mammal> i1m = new C(); // Legal
I1<Animal> i1a = i1m; // Legal
Animal a = i1a.M1(); // Returns a tiger; assigned to animal, good!
Независимо от того, что возвращает C.M1
, это всегда Mammal
и, следовательно, всегда Animal
.
Но это не может бытьlegal:
I1<Giraffe> i1g = i1m; // Not legal
Giraffe g = i1g.M1(); // Returns a tiger; assigned to giraffe, bad!
Первая строка должна быть недопустимой, чтобы вторая строка никогда не выполнялась.
Теперь у вас должно быть достаточно информации, чтобы выяснить, почему контравариантность работает так, как она работает.Помните, что вы всегда можете вернуться к простому примеру и спросить себя: «Если это было законно, какие ошибки я мог бы сделать позже?»Система типов защищает вас от этих ошибок!
Упражнение: Сделайте такой же анализ I2<T>
.Вы понимаете, почему разрешено использовать T
в двух позициях ввода, даже если это out
.(Подсказка: Action
является контравариантным , поэтому он меняет направление совместимости назначений. Что произойдет, если вы измените направление дважды ?)