проблема ковариации / контравариантности в обобщенном делегате C # - PullRequest
0 голосов
/ 24 января 2019

В приведенном ниже коде есть два общих объявления делегата с ковариацией / контравариантностью:

// wrong code since Delegate1 actually needs covariance
public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);

чтобы исправить это, мы можем настроить декларацию Delegate1 на ковариацию

// ok
public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);

но если я настрою "Delegate2<in T>(Delegate1<T> d1)" на "Delegate2<in T>(Delegate1<Delegate1<T>> d1)", приведенный ниже код будет в порядке (независимо от того, Delegate1 - ковариация или контравариантность)

// ok
public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<Delegate1<T>> d1);
// ok too
public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<Delegate1<T>> d1);

Я не совсем уверен в причине ...

1 Ответ

0 голосов
/ 22 февраля 2019

Этот вопрос иллюстрирует некоторые интересные факты о контравариантности и ковариации.

Есть два способа понять эти проблемы. Во-первых, смотреть на это абстрактно и просто смотреть на то, «в каком направлении идут стрелки».

Помните, что «ковариация» означает, что преобразование сохраняет направление стрелки присваиваемости, а «контравариантность» означает, что оно обращено. То есть, если 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...