Covarianza y contravarianza en lenguajes de programación
-
18-09-2019 - |
Pregunta
¿Alguien puede explicarme el concepto de covarianza y contravarianza en la teoría de lenguajes de programación?
Solución
Covarianza es bastante simple y mejor pensado desde la perspectiva de alguna clase de recolección List
. Podemos parametrizar la List
clase con algún parámetro de tipo T
. Es decir, nuestra lista contiene elementos de tipo T
para algunos T
. La lista sería covariante si
S es un subtipo de T IFF List [s] es un subtipo de lista [t
(Donde estoy usando la definición matemática IFF significar si y solo si.)
Eso es un List[Apple]
es un List[Fruit]
. Si hay alguna rutina que acepta un List[Fruit]
Como parámetro, y tengo un List[Apple]
, luego puedo pasar esto como un parámetro válido.
def something(l: List[Fruit]) {
l.add(new Pear())
}
Si nuestra clase de colección List
es mutable, entonces la covarianza no tiene sentido porque podríamos suponer que nuestra rutina podría agregar alguna otra fruta (que no era una manzana) como se indicó anteriormente. Por lo tanto, solo debemos gustarnos inmutable ¡Clases de recolección para ser covariantes!
Otros consejos
Aquí están mis artículos sobre cómo hemos agregado nuevas funciones de varianza a C# 4.0. Comience desde la parte inferior.
http://blogs.msdn.com/ericlippert/archive/tags/covariance+and+Contravariance/default.aspx
Para mayor comodidad, aquí hay una lista ordenada de enlaces a todos los artículos de Eric Lippert sobre varianza:
- Covarianza y contravarianza en C#, parte uno
- Covarianza y contravarianza en C#, Parte Dos: Covarianza de Array
- Covarianza y contravarianza en C#, Parte Tres: Variación de conversión del grupo de métodos
- Covarianza y contravarianza en C#, Parte Cuatro: Real Delegate Variance
- Covarianza y contravarianza en C#, Parte cinco: Las funciones de orden superior lastiman mi cerebro
- Covarianza y contravarianza en C#, Parte seis: Variación de la interfaz
- Covarianza y contravarianza en C# Parte Siete: ¿Por qué necesitamos una sintaxis?
- Covarianza y contravarianza en C#, Parte ocho: opciones de sintaxis
- Covarianza y contravarianza en C#, Parte nueve: cambios de ruptura
- Covarianza y contravarianza en C#, Parte diez: Tratar con la ambigüedad
Se está haciendo una distinción entre covarianza y contravarianza.
Muy aproximadamente, una operación es covariante si conserva el orden de los tipos, y contravariante si reversión este orden.
El orden en sí está destinado a representar tipos más generales como más grandes que los tipos más específicos.
Aquí hay un ejemplo de una situación en la que C# apoya la covarianza. Primero, esta es una variedad 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 derivan de System.Object
en .NET Framework. En otras palabras, System.Object
es muy general o largo escribe. Ahora aquí hay un lugar en el que es compatible con 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[]
, puede almacenar un valor que es de hecho de tipo string[]
.
Piénselo, hasta cierto punto, es lo que esperas, pero de nuevo no lo es. Después de todo, mientras string
deriva de object
, string[]
NO ES derivar de object[]
. El soporte del idioma para la covarianza en este ejemplo hace posible la tarea de todos modos, que es algo que encontrará en muchos casos. Diferencia es una característica que hace que el lenguaje funcione de manera más intuitiva.
Las consideraciones sobre estos temas son extremadamente complicadas. Por ejemplo, según el código anterior, aquí hay dos escenarios que darán lugar a 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 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 un Woman
, el otro es más general y puede trabajar con cualquier tipo derivado de Person
. Sobre el Woman
lado de las cosas, ahora también tienes estos:
delegate void AcceptWomanDelegate(Woman person);
static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
acceptWoman(woman);
}
DoWork
es una función que puede tomar un Woman
y una referencia a una función que también toma un 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 Woman
, y WorkWithPerson
es más grande que WorkWithWoman
.
WorkWithPerson
también se considera más grande que AcceptWomanDelegate
con el propósito de varianza.
Finalmente, tienes estas tres líneas de código:
Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);
A Woman
Se crea la instancia. Entonces se llama a Dowork, pasando al Woman
instancia, así como una referencia al WorkWithWoman
método. Este último es obviamente compatible con el tipo delegado AcceptWomanDelegate
- Un parámetro de tipo Woman
, sin tipo de retorno. Sin embargo, la tercera línea es un poco extraña. El método WorkWithPerson
toma una Person
Como parámetro, no un Woman
, según lo requiera AcceptWomanDelegate
. Sin embargo, WorkWithPerson
es compatible con el tipo delegado. 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
, pasando en un Woman
No puedo estar mal, ¿Correcto?
Por ahora, es posible que se pregunte 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 utilizado object
y string
matrices. 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 lo intenta, encontrará que este no es un escenario compatible en C#. En C# versión 4.0, así como .NET Framework 4.0, se ha limpiado el soporte de varianza en genéricos, y ahora es posible usar las nuevas palabras clave en y afuera 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, lo que permite que funcione la varianza. Pero en el caso de List<T>
, los datos de tipo T
fluye en ambas direcciones: hay métodos en el tipo List<T>
que regresa T
valores y otros que reciben dichos valores.
El punto de estas restricciones direccionales es Para permitir la varianza donde tiene sentido, sino evitar problemas Al igual que el error de tiempo de ejecución mencionado en uno de los ejemplos de matriz anteriores. Cuando los parámetros de tipo están decorados correctamente con en o afuera, el compilador puede verificar y permitir o no permitir su variación en tiempo de compilación. Microsoft se ha esforzado por agregar estas palabras clave a muchas interfaces estándar en .NET Framework, como IEnumerable<T>
:
public interface IEnumerable<out T>: IEnumerable {
// ...
}
Para esta interfaz, el flujo de datos de tipo T
Los objetos están claros: Solo se pueden recuperar de los métodos respaldados por esta interfaz, no pasar a ellos. Como resultado, es posible construir un ejemplo similar al List<T>
Intento descrito anteriormente, pero usando IEnumerable<T>
:
IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;
Este código es aceptable para el compilador C# desde la versión 4.0 porque IEnumerable<T>
es covariante debido a la afuera especificador en el parámetro de tipo T
.
Cuando se trabaja con tipos genéricos, es importante tener en cuenta la varianza y la forma en que el compilador aplica varios tipos de trucos para que su código funcione de la manera que espere.
Hay más que saber sobre la varianza que la cubierta en este capítulo, pero esto será suficiente para que todo el código adicional sea comprensible.
Árbitro:
Bart de Smet tiene una excelente entrada de blog sobre Covariance y contravarianza aquí.
Tanto C# como el CLR permiten covarianza y contravarianza de los tipos de referencia al unir un método a un delegado. La covarianza significa que un método puede devolver un tipo que se deriva del tipo de devolución del delegado. La contravarianza 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 así:
Delegate Object MyCallback (FileStream S);
Es posible construir una instancia de este tipo de delegado vinculado a un método que está prototipado
como esto:
Cadena somemethod (stream s);
Aquí, el tipo de retorno de SomEMethod (cadena) es un tipo que se deriva del tipo de retorno del delegado (objeto); Esta covarianza está permitida. El tipo de parámetro de SomEMethod (transmisión) es un tipo que es una clase base del tipo de parámetro del delegado (FileStream); Esta contravarianza está permitida.
Tenga en cuenta que la covarianza y la contravarianza son compatibles solo para tipos de referencia, no para tipos de valor o para nulo. Entonces, por ejemplo, no puedo vincular el siguiente método al delegado myCallback:
Int32 SomeThermethod (Stream s);
Aunque el tipo de retorno de SomeThermethod (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 cual los tipos de valor y el vacío no se pueden usar para la covarianza y la contravarianza es porque la estructura de memoria para estas cosas varía, mientras que la estructura de memoria para los tipos de referencia siempre es un puntero. Afortunadamente, el compilador C# producirá un error si intenta hacer algo que no sea compatible.