Pregunta

¿Alguien puede explicarme el concepto de covarianza y contravarianza en la teoría de lenguajes de programación?

¿Fue útil?

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

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top