Covarianza y contravarianza (programación)

La covarianza y la contravarianza [1] en programación son formas de transferir la herencia de tipos a los derivados [2] de ellos tipos: contenedores , tipos genéricos , delegados , etc. Los términos se originaron a partir de conceptos similares de la teoría de categorías "covariante" y "funtor contravariante" .

Definiciones

La covarianza es la preservación de la jerarquía de herencia de tipos fuente en tipos derivados en el mismo orden. Entonces, si una clase Cathereda de una clase Animal, entonces es natural asumir que la enumeración IEnumerable<Cat>será descendiente de la enumeración IEnumerable<Animal>. De hecho, la "lista de cinco gatos" es un caso especial de la "lista de cinco animales". En este caso, se dice que el tipo (en este caso, la interfaz genérica) es IEnumerable<T> covariante con su parámetro de tipo T.

La contravarianza es la inversión de la jerarquía de tipos de origen en los tipos derivados. Entonces, si una clase Stringse hereda de la clase Objecty el delegado Action<T>se define como un método que acepta un objeto de tipo T, entonces se Action<Object>hereda del delegado Action<String>y no al revés. De hecho, si "todas las cadenas son objetos", entonces "cualquier método que opere en objetos arbitrarios puede realizar una operación en una cadena", pero no al revés. En tal caso, se dice que el tipo (en este caso, un delegado genérico) es Action<T> contravariante a su parámetro de tipo T.

La falta de herencia entre tipos derivados se llama invariancia .

La contravarianza le permite establecer correctamente el tipo al crear subtipado (subtipado), es decir, establecer un conjunto de funciones que le permite reemplazar otro conjunto de funciones en cualquier contexto. A su vez, la covarianza caracteriza la especialización del código , es decir, la sustitución del antiguo código por uno nuevo en determinados casos. Así, la covarianza y la contravarianza son mecanismos de seguridad de tipo independiente , no excluyentes entre sí, y pueden y deben ser utilizados en lenguajes de programación orientados a objetos [3] .

Uso

Matrices y otros contenedores

En contenedores que permiten objetos de escritura, la covarianza se considera indeseable porque le permite omitir la verificación de tipos. De hecho, considere matrices covariantes. Deje que las clases Caty Doghereden de una clase Animal(en particular, a una variable de tipo Animalse le puede asignar una variable de tipo Cato Dog). Vamos a crear una matriz Cat[]. Gracias al control de tipos, solo los objetos del tipo Caty sus descendientes pueden escribirse en esta matriz. Luego asignamos una referencia a este arreglo a una variable de tipo Animal[](la covarianza de los arreglos lo permite). Ahora en este arreglo, ya conocido como Animal[], escribiremos una variable de tipo Dog. Por lo tanto, Cat[]escribimos en la matriz Dog, sin pasar por el control de tipo. Por lo tanto, es deseable hacer contenedores que permitan escribir invariablemente. Además, los contenedores grabables pueden implementar dos interfaces independientes, un Producer<T> covariante y un Consumer<T> contravariante, en cuyo caso fallará la omisión de verificación de tipos descrita anteriormente.

Dado que la verificación de tipos solo se puede violar cuando se escribe un elemento en el contenedor, para iteradores y colecciones inmutables, la covarianza es segura e incluso útil. Por ejemplo, con su ayuda en el lenguaje C#, a cualquier método que tome un argumento de tipo IEnumerable<Object>se le puede pasar cualquier colección de cualquier tipo, por ejemplo, IEnumerable<String>o incluso List<String>.

Si, en este contexto, el contenedor se usa, por el contrario, solo para escribir en él, y no hay lectura, entonces puede ser contravariante. Por lo tanto, si hay un tipo hipotético WriteOnlyList<T>que hereda List<T>y prohíbe las operaciones de lectura en él, y una función con un parámetro WriteOnlyList<Cat>en el que escribe objetos de tipo , entonces es seguro Catpasarlo ; List<Animal>no List<Object>escribirá nada allí excepto objetos. de la clase heredera, pero intente leer otros objetos que no lo harán.

Tipos de funciones

En lenguajes con funciones de primera clase, existen tipos de funciones genéricas y variables delegadas . Para los tipos de funciones genéricas, la covarianza del tipo de retorno y la contravarianza del argumento son útiles. Por lo tanto, si un delegado se define como "una función que toma una cadena y devuelve un objeto", también se puede escribir una función que toma un objeto y devuelve una cadena: si una función puede tomar cualquier objeto, también puede toma una cuerda; y del hecho de que el resultado de la función es una cadena, se deduce que la función devuelve un objeto.

Implementación en idiomas

C++

C++ ha admitido tipos de devolución covariantes en funciones virtuales anuladas desde el estándar de 1998 :

claseX { }; clase A { público : virtual X * f () { retorna nueva X ; } }; clase Y : público X {}; clase B : público A { público : virtual Y * f () { retornar nueva Y ; } // la covarianza le permite establecer un tipo de retorno refinado en el método anulado };

Los punteros en C++ son covariantes: por ejemplo, a un puntero a una clase base se le puede asignar un puntero a una clase secundaria.

Las plantillas de C++ son, en términos generales, invariantes; las relaciones de herencia de las clases de parámetros no se transfieren a las plantillas. Por ejemplo, un contenedor covariante vector<T>permitiría romper la verificación de tipos. Sin embargo, al usar constructores de copia parametrizados y operadores de asignación, puede crear un puntero inteligente que sea covariante con su parámetro de tipo [4] .

Java

La covarianza del tipo de devolución del método se ha implementado en Java desde J2SE 5.0 . No hay covarianza en los parámetros del método: para anular un método virtual, los tipos de sus parámetros deben coincidir con la definición en la clase principal; de lo contrario, se definirá un nuevo método sobrecargado con estos parámetros en lugar de la anulación.

Las matrices en Java han sido covariantes desde la primera versión, cuando aún no había tipos genéricos en el lenguaje . (Si este no fuera el caso, entonces para usar, por ejemplo, un método de biblioteca que toma una matriz de objetos Object[]para trabajar con una matriz de cadenas String[], primero sería necesario copiarlo en una nueva matriz Object[]). Ya que, como se mencionó arriba, al escribir un elemento en una matriz de este tipo, puede omitir la verificación de tipos, la JVM tiene una verificación adicional en tiempo de ejecución que genera una excepción cuando se escribe un elemento no válido.

Los tipos genéricos en Java son invariables, porque en lugar de crear un método genérico que funcione con Objetos, puede parametrizarlo, convirtiéndolo en un método genérico y conservando el control de tipo.

Al mismo tiempo, en Java, puede implementar una especie de covarianza y contravarianza de tipos genéricos utilizando el carácter comodín y los especificadores calificadores: List<? extends Animal>será covariante del tipo en línea y List<? super Animal> contravariante.

C#

Desde la primera versión de C# , las matrices han sido covariantes. Esto se hizo por compatibilidad con el lenguaje Java [5] . Intentar escribir un elemento del tipo incorrecto en una matriz arroja una excepción en tiempo de ejecución .

Las clases e interfaces genéricas que aparecieron en C# 2.0 se convirtieron, como en Java, en parámetros de tipo invariantes.

Con la introducción de delegados genéricos (parametrizados por tipos de argumentos y tipos de retorno), el lenguaje permitió la conversión automática de métodos ordinarios a delegados genéricos con covarianza en tipos de retorno y contravarianza en tipos de argumento. Por lo tanto, en C# 2.0, se hizo posible un código como este:

void ProcessString ( String s ) { /* ... */ } void ProcessAnyObject ( Object o ) { /* ... */ } String GetString () { /* ... */ } Object GetAnyObject () { /* ... */ } //... Acción < String > proceso = ProcesarCualquierObjeto ; proceso ( miCadena ); // accion legal Func < Objeto > getter = GetString ; Objeto obj = captador (); // accion legal

sin embargo, el código es Action<Object> process = ProcessString;incorrecto y da un error de compilación; de lo contrario, este delegado podría llamarse como process(5), pasando un Int32 a ProcessString.

En C# 2.0 y 3.0, este mecanismo solo permitía escribir métodos simples en delegados genéricos y no podía convertir automáticamente de un delegado genérico a otro. En otras palabras, el código

Func < Cadena > f1 = ObtenerCadena ; Func < Objeto > f2 = f1 ;

no compiló en estas versiones del lenguaje. Por lo tanto, los delegados genéricos en C# 2.0 y 3.0 seguían siendo invariables.

En C# 4.0, se eliminó esta restricción y, a partir de esta versión, el código f2 = f1del ejemplo anterior comenzó a funcionar.

Además, en 4.0 se hizo posible especificar explícitamente la variación de los parámetros de las interfaces genéricas y los delegados. Para ello, se utilizan las palabras clave outy inrespectivamente. Debido a que en un tipo genérico, el uso real del parámetro de tipo solo lo conoce su autor, y debido a que puede cambiar durante el desarrollo, esta solución brinda la mayor flexibilidad sin comprometer la solidez de la tipificación.

Algunas interfaces de biblioteca y delegados se han vuelto a implementar en C# 4.0 para aprovechar estas características. Por ejemplo, la interfaz IEnumerable<T>ahora se define como IEnumerable<out T>, interfaz IComparable<T> como IComparable<in T>, delegado Action<T> como Action<in T>, etc.

Véase también

Notas

  1. La documentación de Microsoft en la copia de archivo ruso fechada el 24 de diciembre de 2015 en Wayback Machine usa los términos covarianza y contravariación .
  2. De ahora en adelante, la palabra "derivado" no significa "heredero".
  3. Castagna, 1995 , Resumen.
  4. Sobre plantillas de covarianza y C++ (8 de febrero de 2013). Consultado el 20 de junio de 2013. Archivado desde el original el 28 de junio de 2013.
  5. Eric Lippert. Covarianza y contravarianza en C#, segunda parte (17 de octubre de 2007). Consultado el 22 de junio de 2013. Archivado desde el original el 28 de junio de 2013.

Literatura

  • Castaña, Giuseppe. Covarianza y contravarianza: conflicto sin causa  //  ACM Trans. programa. Idioma Sist.. - ACM, 1995. - vol. 17 , núm. 3 . — pág. 431-447 . — ISSN 0164-0925 . -doi : 10.1145/ 203095.203096 .