c# - usados - Covarianza y contravarianza en lenguajes de programación.
tipos de lenguaje de programacion y sus caracteristicas (7)
Aquí están mis artículos sobre cómo hemos agregado nuevas características de varianza a C # 4.0. Comenzar desde la parte inferior.
http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx
¿Puede alguien explicarme el concepto de covarianza y contravarianza en la teoría de los lenguajes de programación?
Bart De Smet tiene una excelente entrada de blog sobre covarianza y contravarianza here .
Para mayor comodidad, aquí hay una lista ordenada de enlaces a todos los artículos de Eric Lippert sobre la varianza:
- Covarianza y Contravarianza en C #, Parte Uno
- La covarianza y la contraparte en C #, segunda parte: la coarianza de la matriz
- Covarianza y contraparte en C #, Parte tres: Variación de conversión del grupo de métodos
- Covarianza y Contravarianza en C #, Parte Cuatro: Variación Real del Delegado
- La covarianza y la contraprestación en C #, parte cinco: las funciones de orden superior dañan mi cerebro
- Covarianza y contraparte en C #, parte seis: variación de interfaz
- Covarianza y contraparte en C # parte siete: ¿Por qué necesitamos una sintaxis?
- Covarianza y contraparte en C #, Parte Ocho: Opciones de sintaxis
- Covarianza y Contravarianza en C #, Parte Nueve: Rompiendo Cambios
- Covarianza y Contravarianza en C #, Parte Diez: Cómo lidiar con la ambigüedad
Se hace una distinción entre covarianza y contravarianza .
En términos generales, una operación es covariante si conserva el orden de los tipos, y contravariante si invierte este orden.
El orden en sí mismo pretende representar tipos más generales como más grandes que tipos más específicos.
Aquí hay un ejemplo de una situación en la que C # apoya la covarianza. Primero, esta es una matriz de objetos:
object[] objects=new object[3];
objects[0]=new object();
objects[1]="Just a string";
objects[2]=10;
Por supuesto, es posible insertar diferentes valores en la matriz porque al final todos se derivan de System.Object
en el marco .Net. En otras palabras, System.Object
es un tipo muy general o grande . Ahora aquí hay un lugar donde se admite la covarianza:
asignar un valor de un tipo más pequeño a una variable de un tipo más grande
string[] strings=new string[] { "one", "two", "three" };
objects=strings;
Los objetos variables, que son de tipo object[]
, pueden almacenar un valor que es, de hecho, de tipo string[]
.
Piénsalo, hasta cierto punto, es lo que esperas, pero tampoco lo es. Después de todo, mientras que la string
deriva del object
, la string[]
NO deriva del object[]
. El soporte de lenguaje para la covarianza en este ejemplo hace que la asignación sea posible de todos modos, que es algo que encontrará en muchos casos. La varianza es una característica que hace que el lenguaje funcione de manera más intuitiva.
Las consideraciones en torno a estos temas son extremadamente complicadas. Por ejemplo, según el código anterior, aquí hay dos escenarios que darán como resultado errores.
// Runtime exception here - the array is still of type string[],
// ints can''t be inserted
objects[2]=10;
// Compiler error here - covariance support in this scenario only
// covers reference types, and int is a value type
int[] ints=new int[] { 1, 2, 3 };
objects=ints;
Un ejemplo para el funcionamiento de la contravarianza es un poco más complicado. Imagina estas dos clases:
public partial class Person: IPerson {
public Person() {
}
}
public partial class Woman: Person {
public Woman() {
}
}
Woman
se deriva de la Person
, obviamente. Ahora considera que tienes estas dos funciones:
static void WorkWithPerson(Person person) {
}
static void WorkWithWoman(Woman woman) {
}
Una de las funciones hace algo (no importa qué) con una Woman
, la otra es más general y puede trabajar con cualquier tipo derivado de Person
. En el lado de las cosas de la Woman
, ahora también tienes estas:
delegate void AcceptWomanDelegate(Woman person);
static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
acceptWoman(woman);
}
DoWork
es una función que puede tomar una Woman
y una referencia a una función que también toma una Woman
, y luego pasa la instancia de Woman
al delegado. Considera el polimorfismo de los elementos que tienes aquí. Person
es más grande que la Woman
y WorkWithPerson
es más grande que la WorkWithWoman
del WorkWithWoman
. WorkWithPerson
también se considera más grande que AcceptWomanDelegate
para el propósito de la varianza.
Finalmente, tienes estas tres líneas de código:
Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);
Se crea una instancia de Woman
. Luego se llama a DoWork, pasando la instancia de Woman
y una referencia al método WorkWithWoman
. Este último es obviamente compatible con el tipo de delegado AcceptWomanDelegate
: un parámetro de tipo Woman
, sin tipo de retorno. Sin embargo, la tercera línea es un poco rara. El método WorkWithPerson
toma una Person
como parámetro, no una Woman
, como lo requiere AcceptWomanDelegate
. Sin embargo, WorkWithPerson
es compatible con el tipo de delegado. La contravarianza lo hace posible, por lo que en el caso de los delegados, el tipo más grande WorkWithPerson
se puede almacenar en una variable del tipo más pequeño AcceptWomanDelegate
. Una vez más, es lo intuitivo: si WorkWithPerson
puede trabajar con cualquier Person
, pasar a una Woman
no puede estar mal , ¿verdad?
A estas alturas, quizás se esté preguntando cómo se relaciona todo esto con los genéricos. La respuesta es que la varianza también se puede aplicar a los genéricos. El ejemplo anterior utilizó matrices de object
y string
. Aquí el código usa listas genéricas en lugar de las matrices:
List<object> objectList=new List<object>();
List<string> stringList=new List<string>();
objectList=stringList;
Si prueba esto, encontrará que este no es un escenario compatible en C #. En C # versión 4.0, así como en .Net framework 4.0, se ha eliminado la compatibilidad con la variación en genéricos, y ahora es posible utilizar las nuevas palabras clave de entrada y salida con parámetros de tipo genérico. Pueden definir y restringir la dirección del flujo de datos para un parámetro de tipo particular, permitiendo que la varianza funcione. Pero en el caso de la List<T>
, los datos del tipo T
fluyen en ambas direcciones: existen métodos en el tipo List<T>
que devuelven valores T
y otros que reciben dichos valores.
El punto de estas restricciones direccionales es permitir la variación donde tenga sentido , pero prevenir problemas como el error de tiempo de ejecución mencionado en uno de los ejemplos de matriz anteriores. Cuando los parámetros de tipo están correctamente decorados con entrada o salida , el compilador puede verificar, y permitir o rechazar, su varianza en el momento de la compilación . Microsoft se ha esforzado por agregar estas palabras clave a muchas interfaces estándar en el marco .Net, como IEnumerable<T>
:
public interface IEnumerable<out T>: IEnumerable {
// ...
}
Para esta interfaz, el flujo de datos de T
objetos de tipo T
es claro: solo se pueden recuperar de los métodos compatibles con esta interfaz, no se pueden pasar a ellos . Como resultado, es posible construir un ejemplo similar al intento de List<T>
descrito anteriormente, pero utilizando IEnumerable<T>
:
IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;
Este código es aceptable para el compilador de C # desde la versión 4.0 porque IEnumerable<T>
es covariante debido al especificador de salida en el parámetro de tipo T
Cuando se trabaja con tipos genéricos, es importante tener en cuenta la variación y la forma en que el compilador está aplicando varios tipos de trucos para que su código funcione de la manera que espera.
Hay más que saber sobre la variación de lo que se trata en este capítulo, pero esto será suficiente para que todo el código sea comprensible.
Árbitro:
Tanto C # como el CLR permiten la covarianza y la contra-varianza de los tipos de referencia cuando se vincula un método a un delegado. La covarianza significa que un método puede devolver un tipo que se deriva del tipo de retorno del delegado. Contra-varianza significa que un método puede tomar un parámetro que es una base del tipo de parámetro del delegado. Por ejemplo, dado un delegado definido de esta manera:
delegar objeto MyCallback (FileStream s);
es posible construir una instancia de este tipo de delegado vinculado a un método que está prototipado
Me gusta esto:
String SomeMethod (Stream s);
Aquí, el tipo de retorno de SomeMethod (String) es un tipo que se deriva del tipo de retorno del delegado (Object); esta covarianza es permitida El tipo de parámetro de SomeMethod (Stream) es un tipo que es una clase base del tipo de parámetro del delegado (FileStream); Se permite esta contra-varianza.
Tenga en cuenta que la covarianza y la contra-varianza se admiten solo para tipos de referencia, no para tipos de valor o para nulos. Entonces, por ejemplo, no puedo vincular el siguiente método al delegado de MyCallback:
Int32 SomeOtherMethod (Stream s);
Aunque el tipo de retorno de SomeOtherMethod (Int32) se deriva del tipo de retorno de MyCallback (Objeto), esta forma de covarianza no está permitida porque Int32 es un tipo de valor.
Obviamente, la razón por la que los tipos de valor y el vacío no se pueden usar para la covarianza y la contravarianza se debe a que la estructura de la memoria para estas cosas varía, mientras que la estructura de la memoria para los tipos de referencia siempre es un indicador. Afortunadamente, el compilador de C # producirá un error si intenta hacer algo que no es compatible.
La covarianza es bastante simple y mejor pensada desde la perspectiva de una List
clase de colección. Podemos parametrizar la clase List
con algún tipo de parámetro T
Es decir, nuestra lista contiene elementos de tipo T
para algunas T
La lista sería covariante si
S es un subtipo de T iff Lista [S] es un subtipo de Lista [T]
(Donde estoy usando la definición matemática iff para indicar si y solo si ).
Es decir, una List[Apple]
es una List[Fruit]
. Si hay alguna rutina que acepta una List[Fruit]
como parámetro, y tengo una List[Apple]
, entonces puedo pasar esto como un parámetro válido.
def something(l: List[Fruit]) {
l.add(new Pear())
}
Si nuestra List
clases de colección es mutable, entonces la covarianza no tiene sentido porque podemos suponer que nuestra rutina podría agregar alguna otra fruta (que no era una manzana) como se indicó anteriormente. Por lo tanto, ¡solo nos gustaría que las clases de colección inmutables sean covariantes!