c# .net covariance contravariance

c# - ¿Por qué es la acción<Acción<T>> covariante?



.net covariance (4)

Esto es algo que estoy teniendo dificultades para envolver mi cabeza. Entiendo que la Action<T> es contravariante y probablemente se declare como tal.

internal delegate void Action<in T>(T t);

Sin embargo, no entiendo por qué una Action<Action<T>> es covariante. T todavía no está en una posición de salida . Realmente apreciaría si alguien pudiera tratar de explicar el razonamiento / lógica detrás de esto.

Hojeé un poco y encontré this blog que intenta explicarlo. En particular, no seguí lo que significaba aquí en la subsección "Explicación para la covarianza de la entrada".

Es lo mismo si se reemplaza el par "Derivado -> Base" por el par "Acción -> Acción".


Considera este ejemplo:

string s = "Hello World!"; object o = s; // fine

Si lo envolvemos con Action

Action<string> s; Action<object> o; s = o; // reverse of T''s polymorphism

Esto se debe a que in el efecto del parámetro es hacer que la jerarquía asignable del tipo genérico sea lo contrario de la jerarquía del parámetro del tipo . Por ejemplo, si esto es válido:

TDerived t1; TBase t2; t2 = t1; // denote directly assignable without operator overloading

entonces

Action<TDerived> at1; Action<TBase> at2; at1 = at2; // contravariant

es válida. Entonces

Action<Action<TDerived>> aat1; Action<Action<TBase>> aat2; aat2 = aat1; // covariant

y también

Action<Action<Action<TDerived>>> aaat1; Action<Action<Action<TBase>>> aaat2; aaat1 = aaat2; // contravariant

y así.

El efecto y la forma en que covariant y contravariant funcionan en comparación con la asignación normal se explican en http://msdn.microsoft.com/en-us/library/dd799517.aspx . En resumen, la asignación covariante funciona como el polimorfismo OOP normal, y la contravariante funciona hacia atrás.


Considera esto:

class Base { public void dosth(); } class Derived : Base { public void domore(); }

Usando la acción de T:

// this is all clear Action<Base> a1 = x => x.dosth(); Action<Derived> b1 = a1;

Ahora:

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()); };

Lo que funcionaría, porque todavía puedes tratar el resultado del new Derived() como una Base . Ambas clases pueden dosth() .

Ahora, en este caso:

Action<Action<Base>> a2 = x => { x(new Derived()); }; Action<Action<Derived>> b2 = x => { x(new Derived()); };

Todavía funcionaría, al usar new Derived() . Sin embargo, eso no se puede decir en general y por lo tanto es ilegal. Considera esto:

Action<Action<Base>> a2 = x => { x(new Base()); }; Action<Action<Derived>> b2 = x => { x(new Base()); };

Error: Action<Action<Derived>> espera que domore() exista, pero Base solo entrega dosth() .


De acuerdo, primero que todo, sea claro lo que quiere decir al decir que la Action<Action<T>> es covariante . Quiere decir que la siguiente afirmación es válida:

  • Si un objeto del tipo de referencia X puede asignarse a una variable del tipo de referencia Y, entonces un objeto del tipo Action<Action<X>> puede asignarse a una variable del tipo de referencia Action<Action<Y>> .

Bueno, vamos a ver si eso funciona. Supongamos que tenemos clases de Fish y Animal con la herencia obvia.

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);

¿Qué hace eso? Pasamos un delegado a DoSomething to Meta. Eso crea un nuevo pez, y luego DoSomething hace que los peces naden. No hay problema.

Hasta ahora tan bueno. Ahora la pregunta es, ¿por qué debería ser legal?

Action<Action<Animal>> aaa = aaf;

Bueno, veamos que pasa si lo permitimos:

aaa(af);

¿Lo que pasa? Lo mismo que antes, obviamente.

¿Podemos hacer que algo salga mal aquí? ¿Qué pasa si pasamos algo que no sea af a aaa , recordando que al hacerlo se lo pasaremos a Meta ?

Bueno, ¿qué podemos pasar a aaa ? Cualquier Action<Animal> :

aaa( (Animal animal) => { animal.Feed(); } );

¿Y que pasa? Pasamos el delegado a Meta , que invoca al delegado con un nuevo pez y lo alimentamos. No hay problema.

T todavía no está en una posición de salida. Realmente apreciaría si alguien pudiera tratar de explicar el razonamiento / lógica detrás de esto.

La posición de "entrada / salida" es una mnemotécnica; El tipo covariante tiende a tener la T en la posición de salida y el tipo contravariante tiende a tener la T en la posición de entrada, pero eso no es universalmente cierto. Para la mayoría de los casos, es cierto, por eso elegimos las palabras clave de out y out . Pero lo que realmente importa es que los tipos solo se pueden usar de una manera segura.

Aquí hay otra manera de pensar en ello. La covarianza conserva la dirección de una flecha . Dibuja una string --> object flechas string --> object , puede dibujar la flecha "misma" IEnumerable<string> --> IEnumerable<object> . La contrariedad invierte la dirección de una flecha . Aquí la flecha es X --> Y significa que una referencia a X puede almacenarse en una variable de tipo Y:

Fish --> Animal Action<Fish> <-- Action<Animal> Action<Action<Fish>> --> Action<Action<Animal>> Action<Action<Action<Fish>>> <-- Action<Action<Action<Animal>>> ...

¿Ves cómo funciona eso? La Action envolver alrededor de ambos lados invierte la dirección de la flecha; eso es lo que significa "contravariante": a medida que los tipos varían, las flechas van en la dirección contraria a la opuesta. Obviamente, invertir la dirección de una flecha dos veces es lo mismo que preservar la dirección de una flecha .

OTRAS LECTURAS:

Los artículos de mi blog que escribí mientras diseñaba la característica. Comience desde la parte inferior:

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx

Una pregunta reciente sobre cómo se determina que la varianza es segura para el tipo por el compilador:

Reglas de variación en C #


Hay un árbol de herencia, con el objeto como la raíz. Un camino en el árbol normalmente se vería así.

object -> Base -> Child

Un Objeto de un tipo superior en el árbol siempre se puede asignar a una variable de un tipo inferior en el árbol. La covarianza en los tipos genéricos significa que los tipos realizados están relacionados de una manera que sigue al árbol

object -> IEnumerable<object> -> IEnumerable<Base> -> IEnumerable<Child>

object -> IEnumerable<object> -> IEnumerable<IEnumerable<object> -> ...

La contraparte significa que los tipos realizados están relacionados de una manera que invierte el árbol.

object -> Action<Child> -> Action<Base> -> Action<object>

Cuando vas un nivel más profundo, tienes que invertir el árbol de nuevo

object -> Action<Action<object>> -> Action<Action<Base>> -> Action<Action<Child>> -> Action<object>

ps con contravarianza, la jerarquía de objetos ya no es un árbol, sino que de hecho es un gráfico acíclico dirigido