Почему Action <Action <T>> ковариант?
Это то, с чем я с трудом переворачиваю голову. Я понимаю, что Action<T>
является контравариантным и, вероятно, объявлен как таковой.
internal delegate void Action<in T>(T t);
Однако я не понимаю , почему a Action<Action<T>>
является ковариантным. T
по-прежнему не находится в позиции вывода. Я бы очень признателен, если кто-то попытается объяснить логику/логику.
Я немного потянулся и нашел этот блог, который пытается объяснить это. В частности, я не совсем понял, что здесь подразумевается под подразделом "Объяснение для ковариации ввода".
Это то же самое, если пара "Derived → Base" заменена парой "Action → Action".
Ответы
Ответ 1
ОК, поэтому прежде всего давайте понять, что вы подразумеваете, говоря, что Action<Action<T>>
является ковариантным. Вы имеете в виду следующее утверждение:
- Если объекту ссылочного типа X может быть присвоена переменная ссылочного типа Y, то объект типа
Action<Action<X>>
может быть назначен переменной ссылочного типа Action<Action<Y>>
.
Хорошо, посмотрим, работает ли это. Предположим, что мы имеем классы Fish
и Animal
с очевидным наследованием.
static void DoSomething(Fish fish)
{
fish.Swim();
}
static void Meta(Action<Fish> action)
{
action(new Fish());
}
...
Action<Action<Fish>> aaf = Meta;
Action<Fish> af = DoSomething;
aaf(af);
Что это делает? Мы передаем делегата DoSomething в Meta. Это создает новую рыбу, а затем DoSomething заставляет рыбу плыть. Нет проблем.
Пока все хорошо. Теперь вопрос в том, почему это должно быть законным?
Action<Action<Animal>> aaa = aaf;
Хорошо, посмотрим, что произойдет, если мы разрешим это:
aaa(af);
Что происходит? То же, что и раньше, очевидно.
Можем ли мы что-то сделать не так? Что делать, если мы передаем что-то отличное от af
до aaa
, помня, что это будет передано вместе с Meta
.
Хорошо, что мы можем передать на aaa
? Любой Action<Animal>
:
aaa( (Animal animal) => { animal.Feed(); } );
И что происходит? Мы передаем делегату Meta
, который вызывает делегата с новой рыбой, и мы кормим рыбу. Нет проблем.
T все еще не находится в выходной позиции. Я бы очень признателен, если кто-то попытается объяснить логику/логику.
Позиция "позиция ввода/вывода" является мнемонической; ковариантный тип имеет тенденцию иметь T в выходной позиции, а контравариантный тип имеет тенденцию иметь T во входном положении, но это не универсально. В большинстве случаев это правда, поэтому мы выбрали in
и out
в качестве ключевых слов. Но важно то, что типы могут использоваться только в виде типов.
Вот еще один способ подумать об этом. Ковариация сохраняет направление стрелки. Вы нарисуете стрелку string --> object
, вы можете нарисовать "ту же" стрелку IEnumerable<string> --> IEnumerable<object>
. Контравариантность меняет направление стрелки. Здесь стрелка X --> Y
означает, что ссылка на X может храниться в переменной типа Y:
Fish --> Animal
Action<Fish> <-- Action<Animal>
Action<Action<Fish>> --> Action<Action<Animal>>
Action<Action<Action<Fish>>> <-- Action<Action<Action<Animal>>>
...
Посмотрите, как это работает? Обтекание Action
вокруг обеих сторон меняет направление стрелки; что "контравариантный" означает: по мере изменения типов стрелки идут в противоположном направлении. Очевидно, что обратное направление стрелки дважды является тем же самым, что и сохранение направления стрелки.
ДАЛЬНЕЙШИЕ ЧТЕНИЯ:
Мои статьи в блоге, которые я написал при разработке функции. Начните снизу:
http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx
Недавний вопрос о том, как дисперсия определяется как тип typeable компилятором:
Правила отклонения в С#
Ответ 2
Рассмотрим следующий пример:
string s = "Hello World!";
object o = s; // fine
Если мы обернем его с помощью Action
Action<string> s;
Action<object> o;
s = o; // reverse of T polymorphism
Это связано с тем, что эффект параметра in
делает иерархию назначаемого типа назначением обратной иерархии параметров типа. Например, если это действительно:
TDerived t1;
TBase t2;
t2 = t1; // denote directly assignable without operator overloading
затем
Action<TDerived> at1;
Action<TBase> at2;
at1 = at2; // contravariant
. Тогда
Action<Action<TDerived>> aat1;
Action<Action<TBase>> aat2;
aat2 = aat1; // covariant
а также
Action<Action<Action<TDerived>>> aaat1;
Action<Action<Action<TBase>>> aaat2;
aaat1 = aaat2; // contravariant
и т.д.
Эффект и то, как ковариантные и контравариантные работы сравниваются с обычным назначением, объясняются в http://msdn.microsoft.com/en-us/library/dd799517.aspx. Короче говоря, ковариантное присваивание работает как обычный полиморфизм ООП, а контравариант работает назад.
Ответ 3
Рассмотрим это:
class Base { public void dosth(); }
class Derived : Base { public void domore(); }
Использование действия T:
// this is all clear
Action<Base> a1 = x => x.dosth();
Action<Derived> b1 = a1;
Сейчас:
Action<Action<Derived>> a = x => { x(new Derived()); };
Action<Action<Base>> b = a;
// the line above is basically this:
Action<Action<Base>> b = x => { x(new Derived()); };
Что будет работать, потому что вы можете обрабатывать результат new Derived()
как Base
. Оба класса могут dosth()
.
Теперь, в этом случае:
Action<Action<Base>> a2 = x => { x(new Derived()); };
Action<Action<Derived>> b2 = x => { x(new Derived()); };
Он все равно будет работать при использовании new Derived()
. Однако это нельзя сказать вообще и, следовательно, является незаконным. Рассмотрим это:
Action<Action<Base>> a2 = x => { x(new Base()); };
Action<Action<Derived>> b2 = x => { x(new Base()); };
Ошибка: Action<Action<Derived>>
ожидает domore()
, но Base
выполняет только dosth()
.
Ответ 4
Существует дерево наследования с объектом в качестве корня. Путь на дереве обычно выглядит примерно так:
object -> Base -> Child
Объект типа, выше на дереве, может быть назначен переменной типа ниже на дереве. Ковариация в общих типах означает, что реализованные типы связаны способом, который следует за деревом
object -> IEnumerable<object> -> IEnumerable<Base> -> IEnumerable<Child>
object -> IEnumerable<object> -> IEnumerable<IEnumerable<object> -> ...
Контравариантность означает, что реализованные типы связаны таким образом, что обращается к дереву.
object -> Action<Child> -> Action<Base> -> Action<object>
Когда вы идете на уровень глубже, вам нужно снова изменить дерево
object -> Action<Action<object>> -> Action<Action<Base>> -> Action<Action<Child>> -> Action<object>
p.s. с контравариантностью иерархия объектов больше не является деревом, а является фактически ориентированным ациклическим графом