Этот вопрос иллюстрирует некоторые интересные факты о контравариантности и ковариации.
Есть два способа понять эти проблемы. Во-первых, смотреть на это абстрактно и просто смотреть на то, «в каком направлении идут стрелки».
Помните, что «ковариация» означает, что преобразование сохраняет направление стрелки присваиваемости, а «контравариантность» означает, что оно обращено. То есть, если A -> B означает «Объект типа A может быть присвоен переменной типа B», то:
Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>
Выполнение последовательности сохраняет направление стрелки; это «ко-вариант». «Co» означает «идти с» здесь. Сравнение меняет направление, оно «против», что означает «идти против».
Это должно иметь смысл; последовательность жирафов может быть использована там, где требуется последовательность животных. И если у вас есть вещь, которая может сравнивать любых животных, то она может сравнивать любых жирафов.
Способ понять, почему два последних фрагмента программы являются допустимыми, заключается в том, что в случае, когда у вас есть два вложенных ковариантных типа, вы говорите: «идите в одном направлении, а затем в том же направлении, что и это ", что то же самое, что" идти в том же направлении ". Когда вы вкладываете два контравариантных типа, вы говорите «идите в противоположном направлении, затем идите в противоположном направлении», что аналогично «идти в том же направлении»! Контравариантность меняет направление стрелки. Поворот стрелки дважды возвращает ее в исходное положение!
Но мне не нравится понимать эти вещи. Скорее, мне нравится думать о вопросе «что может пойти не так, если мы сделаем это по-другому?»
Итак, давайте посмотрим на ваши четыре случая и спросим "что может пойти не так"?
Я внесу небольшие изменения в ваши типы.
public delegate void D1<in T>(T t);
public delegate void D2<in T>(D1<T> d1t); // This is wrong.
Почему D2 не так? Ну, что может пойти не так, если мы это допустим?
// This is a cage that can hold any animal.
AnimalCage cage = new AnimalCage();
// Let's make a delegate that inserts an animal into the cage.
D1<Animal> d1animal = (Animal a) => cage.InsertAnimal(a);
// Now lets use the same delegate to insert a tiger. That's fine!
D1<Tiger> d1tiger = d1animal;
d1tiger(new Tiger());
Теперь в клетке тигр, с ним все в порядке; клетка может содержать любое животное.
Но теперь давайте посмотрим, как обстоят дела с D2. Предположим, что объявление D2 было законным.
// This line is fine; we're assigning D1<Animal> to D1<Tiger>
// and it is contravariant.
D2<Animal> d2animal = (D1<Animal> d1a) => {d1tiger = d1a;};
// An aquarium can hold any fish.
Aquarium aquarium = new Aquarium();
// Let's make a delegate that puts a fish into an aquarium.
D1<Fish> d1fish = (Fish f) => aquarium.AddFish(f);
// This conversion is fine, because D2 is contravariant.
D2<Fish> d2fish = d2animal;
// D2<Fish> takes a D1<Fish> so we should be able to do this:
d2fish(d1fish);
// Lets put another tiger in the cage.
d1tiger(new Tiger());
OK, каждая строка в этой программе имела тип safe . Но проследите через логику. Что случилось? Когда мы вызвали d1tiger в последней строке, что это равнялось? Ну, d2fish (d1fish) назначает d1fish для ... d1tiger. Но d1tiger напечатан как D1<Tiger>
, а не D1<Fish>
. Итак, мы присвоили значение переменной неправильного типа. Вот что случилось потом? Мы назвали d1Tiger с новым тигром, а d1Tiger положил тигра в аквариум!
Каждая из этих строк была безопасна для типов, но программа не была безопасной для типов, так что мы должны сделать вывод? Объявление D2 было небезопасно . И именно поэтому компилятор выдает ошибку.
На основании этого анализа мы знаем, что D2<in T>(D1<T>)
должно быть ошибочным.
Упражнение 1 :
delegate T D3<out T>();
delegate void D4<in T>(D3<T> d3t);
Пройдите ту же логику, что и я, но на этот раз убедите себя, что этот никогда не приводит к проблеме системы типов.
Как только вы это поняли, сделайте сложные:
Упражнение 2 : Пройдите логику снова, но на этот раз с
delegate void D5<in T>(D3<D3<T>> d3d3t);
Опять же, убедите себя, что это законно, и что этот случай логически совпадает с Упражнением 1.
Упражнение 3 : И последнее, самое сложное:
delegate void D6<in T>(D1<D1<T>> d1d1t);
Убедите себя, что это законно, потому что D1<D1<T>>
дважды меняет стрелку и, следовательно, логически то же самое, что и упражнение 1.