En programación informática , la varianza de tipos es la relación entre los subtipos de un tipo compuesto (por ejemplo, List[Int]) y los subtipos de sus componentes (por ejemplo, Int). La varianza elegida por un lenguaje determina la relación entre, por ejemplo, una lista de Caty una lista de Animal, o una función que devuelveCat y una función que devuelve Animal.
Si el tipoCat es un subtipo de Animal, entonces una expresión de tipo debería ser sustituible dondequiera que se use una expresión de tipo . Dependiendo de la varianza del constructor de tipo , la relación de subtipo de los tipos simples puede conservarse, invertirse o ignorarse para los tipos complejos respectivos. En muchos lenguajes de programación, por ejemplo, "lista de Cat" será un subtipo de "lista de Animal", porque el constructor de tipo lista es covariante . Esto significa que la relación de subtipo de los tipos simples se conserva para los tipos complejos. Por otro lado, "función de Animal a String" es un subtipo de "función de Cat a String" porque el constructor de tipo función es contravariante en el tipo de parámetro . Aquí, la relación de subtipo de los tipos simples se invierte para los tipos complejos.CatAnimal
Un diseñador de lenguajes de programación tendrá en cuenta la varianza al diseñar reglas de tipado para características del lenguaje como arreglos , herencia y tipos de datos genéricos . Al hacer que los constructores de tipos sean covariantes o contravariantes en lugar de invariantes, se aceptarán más programas como bien tipados. Por otro lado, a los programadores a menudo les resulta poco intuitiva la contravarianza, y el seguimiento preciso de la varianza para evitar errores de tipo en tiempo de ejecución puede dar lugar a reglas de tipado complejas.
Para mantener el sistema de tipos simple y permitir programas útiles, un lenguaje puede tratar un constructor de tipos como invariante, incluso si sería seguro considerarlo variante, o tratarlo como covariante, aunque eso podría violar la seguridad de tipos .
Etimología
Estos términos provienen de la noción de functores covariantes y contravariantes en la teoría de categorías . Consideremos la categoríacuyos objetos son tipos y cuyos morfismos representan la relación de subtipo ≤. (Este es un ejemplo de cómo cualquier conjunto parcialmente ordenado puede considerarse como una categoría). Entonces, por ejemplo, el constructor de tipo de función toma dos tipos p y r y crea un nuevo tipo p → r ; por lo que toma objetos ena objetos enSegún la regla de subtipado para tipos de función, esta operación invierte ≤ para el primer parámetro y lo conserva para el segundo, por lo que es un functor contravariante en el primer parámetro y un functor covariante en el segundo.
Definición formal
Supongamos Aque y Bson tipos, y I<U>denota la aplicación de un constructor de tipoI con argumento de tipo U. Dentro del sistema de tipos de un lenguaje de programación, una regla de tipado para un constructor de tipo Ies:
- covariante si conserva el orden de los tipos (≤) , que ordena los tipos de más específicos a más genéricos: Si
A ≤ B, entonces ;I<A> ≤ I<B> - contravariante si invierte este orden: Si
A ≤ B, entonces ;I<B> ≤ I<A> - bivariante si se cumplen ambas condiciones (es decir, si
A ≤ B, entonces ); [ nota 1 ]I<A> ≡ I<B> - variante si es covariante, contravariante o bivariante;
- invariante o no variante si no es variante.
El artículo analiza cómo se aplica esto a algunos constructores de tipos comunes.
Ejemplos de C#
Por ejemplo, en C# , si Cates un subtipo de Animal, entonces:
IEnumerable<Cat>es un subtipo de . La subtipificación se conserva porque es covariante en .IEnumerable<Animal>IEnumerable<T>TAction<Animal>es un subtipo de . La subtipificación se invierte porque es contravariante en .Action<Cat>Action<T>T- Ni uno ni otro es un subtipo del otro, porque es invariante en .
IList<Cat>IList<Animal>IList<T>T
La varianza de una interfaz genérica de C# se declara colocando el atributo out(covariante) o in(contravariante) en (cero o más de) sus parámetros de tipo. [ 1 ] : 144 Las interfaces anteriores se declaran como , , y . Los tipos con más de un parámetro de tipo pueden especificar diferentes varianzas en cada parámetro de tipo. Por ejemplo, el tipo delegado representa una función con un parámetro de entrada contravariante de tipo y un valor de retorno covariante de tipo . [ 2 ] [ 1 ] : 145 El compilador comprueba que todos los tipos estén definidos y se utilicen de forma coherente con sus anotaciones, y de lo contrario señala un error de compilación.IEnumerable<outT>Action<inT>IList<T>Func<inT,outTResult>TTResult
Las reglas de tipado para la varianza de interfaz garantizan la seguridad de tipos. Por ejemplo, un representa una función de primera clase que espera un argumento de tipo , [ 1 ] : 144 y una función que puede manejar cualquier tipo de animal siempre se puede usar en lugar de una que solo puede manejar gatos.Action<T>T
Matrices
Los tipos de datos de solo lectura (fuentes) pueden ser covariantes; los tipos de datos de solo escritura (sumideros) pueden ser contravariantes. Los tipos de datos mutables que actúan como fuentes y sumideros deben ser invariantes. Para ilustrar este fenómeno general, consideremos el tipo de matriz . Para este tipo podemos hacer el tipo , que es una "matriz de animales". Para los fines de este ejemplo, esta matriz admite elementos tanto de lectura como de escritura.AnimalAnimal[]
Tenemos la opción de tratar esto de cualquiera de las siguientes maneras:
- covariante: a es un ;
Cat[]Animal[] - contravariante: un es un ;
Animal[]Cat[] - invariante: un no es un y un no es un .
Animal[]Cat[]Cat[]Animal[]
Si queremos evitar errores de tipo, solo la tercera opción es segura. Claramente, no todos pueden tratarse como si fueran un , ya que un cliente que lea del array esperará un , pero un puede contener, por ejemplo, un . Por lo tanto, la regla contravariante no es segura.Animal[]Cat[]CatAnimal[]Dog
Por el contrario, un no puede ser tratado como un . Siempre debería ser posible colocar un en un . Con arreglos covariantes, esto no puede garantizarse como seguro, ya que el almacenamiento subyacente podría ser en realidad un arreglo de gatos. Por lo tanto, la regla covariante tampoco es segura : el constructor del arreglo debería ser invariante . Tenga en cuenta que esto solo es un problema para arreglos mutables; la regla covariante es segura para arreglos inmutables (de solo lectura). Del mismo modo, la regla contravariante sería segura para arreglos de solo escritura.Cat[]Animal[]DogAnimal[]
Matrices covariantes en Java y C#
Las primeras versiones de Java y C# no incluían genéricos, también conocidos como polimorfismo paramétrico . En este contexto, hacer que los arreglos sean invariantes descarta la posibilidad de desarrollar programas polimórficos útiles.
Por ejemplo, considere escribir una función para barajar un array, o una función que compruebe la igualdad de dos arrays utilizando el método `Object .` en los elementos. La implementación no depende del tipo exacto de elemento almacenado en el array, por lo que debería ser posible escribir una única función que funcione con todos los tipos de arrays. Es fácil implementar funciones de este tipo:equals
boolean equalArrays ( Object [] a1 , Object [] a2 ); void shuffleArray ( Object [] a );Sin embargo, si los tipos de array se trataran como invariantes, solo sería posible llamar a estas funciones en un array del tipo exacto . No se podría, por ejemplo, barajar un array de cadenas.Object[]
Por lo tanto, tanto Java como C# tratan los tipos de array de forma covariante. Por ejemplo, en Java es un subtipo de , y en C# es un subtipo de .String[]Object[]string[]object[]
Como se mencionó anteriormente, los arreglos covariantes generan problemas al escribir en ellos. Java [ 3 ] : 126 y C# solucionan esto marcando cada objeto de arreglo con un tipo al crearlo. Cada vez que se almacena un valor en un arreglo, el entorno de ejecución verifica que el tipo en tiempo de ejecución del valor sea igual al tipo en tiempo de ejecución del arreglo. Si hay una discrepancia, se lanza una excepción (Java) [ 3 ] : 126 o (C#):ArrayStoreExceptionArrayTypeMismatchException
// a es un array de un solo elemento de tipo String String [] a = new String [ 1 ] ;// b es un array de objetos Object [] b = a ;// Asigna un entero (int) a b. Esto sería posible si b fuera realmente // un array de objetos, pero como en realidad es un array de cadenas, // obtendremos una java.lang.ArrayStoreException en tiempo de ejecución. b [ 0 ] = 1 ;En el ejemplo anterior, se puede leer del array (b) sin problemas. El problema surge al intentar escribir en él.
Una desventaja de este enfoque es que deja abierta la posibilidad de un error en tiempo de ejecución que un sistema de tipos más estricto podría haber detectado en tiempo de compilación. Además, perjudica el rendimiento, ya que cada escritura en un array requiere una comprobación adicional en tiempo de ejecución.
Con la adición de genéricos, Java [ 3 ] : 126–129 y C# ahora ofrecen formas de escribir este tipo de función polimórfica sin depender de la covarianza. Las funciones de comparación y reordenamiento de matrices pueden recibir tipos parametrizados.
< T > boolean equalArrays ( T [] a1 , T [] a2 ); < T > void shuffleArray ( T [] a );Como alternativa, para garantizar que un método de C# acceda a una colección de forma de solo lectura, se puede utilizar la interfaz en lugar de pasarle una matriz .IEnumerable<object>object[]
Tipos de función
Los lenguajes con funciones de primera clase tienen tipos de funciones como "una función que espera un Cat y devuelve un Animal" (escrita en sintaxis OCaml o en sintaxis C# ).cat->animalFunc<Cat,Animal>
Esos lenguajes también necesitan especificar cuándo un tipo de función es un subtipo de otro ; es decir, cuándo es seguro usar una función de un tipo en un contexto que espera una función de un tipo diferente. Es seguro sustituir una función f por una función g si f acepta un tipo de argumento más general y devuelve un tipo más específico que g . Por ejemplo, las funciones de tipo , , y se pueden usar dondequiera que se esperara a. (Esto se puede comparar con el principio de robustez de la comunicación: "sé liberal en lo que aceptas y conservador en lo que produces"). La regla general es:animal->catcat->catanimal->animalcat->animal
- siy.
Utilizando la notación de reglas de inferencia, la misma regla se puede escribir como:
En otras palabras, el constructor de tipo → es contravariante en el tipo de parámetro (entrada) y covariante en el tipo de retorno (salida) . Esta regla fue enunciada formalmente por primera vez por John C. Reynolds [ 4 ] y popularizada posteriormente en un artículo de Luca Cardelli [ 5 ] .
Cuando se trabaja con funciones que toman funciones como argumentos , esta regla se puede aplicar varias veces. Por ejemplo, al aplicar la regla dos veces, vemos quesi. En otras palabras, el tipoes covariante en la posición dePara tipos complejos, puede resultar confuso rastrear mentalmente por qué una especialización de tipo dada es o no es segura en cuanto a tipos, pero es fácil calcular qué posiciones son covariantes y contravariantes: una posición es covariante si está en el lado izquierdo de un número par de flechas que se aplican a ella.
Herencia en lenguajes orientados a objetos
En un lenguaje de programación orientada a objetos (POO), cuando una subclase sobrescribe un método de una superclase, el compilador debe verificar que el método sobrescritor tenga el tipo correcto. Si bien algunos lenguajes requieren que el tipo coincida exactamente con el tipo de la superclase (invariancia), también es seguro en cuanto a tipos permitir que el método sobrescritor tenga un tipo "mejor". Según la regla de subtipado habitual para los tipos de función, esto significa que el método sobrescritor debe devolver un tipo más específico (covarianza del tipo de retorno) y aceptar un argumento más general (contravarianza del tipo de parámetro). En la notación del Lenguaje Unificado de Modelado (UML), las posibilidades son las siguientes (donde la Clase B es la subclase que extiende la Clase A, que es la superclase):
- Varianza y sobrescritura de métodos: descripción general
Subtipado del tipo de parámetro/retorno del método.
Invariancia . La firma del método que lo reemplaza no cambia.
Tipo de retorno covariante . La relación de subtipos tiene la misma dirección que la relación entre la Clase A y la Clase B.
Tipo de parámetro contravariante . La relación de subtipificación es en la dirección opuesta a la relación entre ClaseA y ClaseB.
Tipo de parámetro covariante . No es seguro en cuanto a tipos.
Como ejemplo concreto, supongamos que estamos escribiendo una clase para modelar un refugio de animales . Asumimos que es una subclase de y que tenemos una clase base (usando la sintaxis de Java ).CatAnimal

clase Refugio de animales {Animal getAnimalForAdoption () { // ... } void putAnimal ( Animal animal ) { //... } }Ahora la pregunta es: si creamos una subclase , ¿qué tipos podemos asignar a y ?AnimalSheltergetAnimalForAdoptionputAnimal
Tipo de retorno del método covariante
En un lenguaje que permite tipos de retorno covariantes , una clase derivada puede sobrescribir el método para devolver un tipo más específico:getAnimalForAdoption

clase CatShelter extiende AnimalShelter {Cat getAnimalForAdoption () { return new Cat (); } }Entre los lenguajes de programación orientada a objetos más comunes, Java , C++ y C# (a partir de la versión 9.0 [ 6 ] ) admiten tipos de retorno covariantes. La incorporación del tipo de retorno covariante fue una de las primeras modificaciones del lenguaje C++ aprobadas por el comité de estándares en 1998. [ 7 ] Scala y D también admiten tipos de retorno covariantes.
Tipo de parámetro del método contravariante
De manera similar, es seguro en cuanto a tipos permitir que un método que sobrescribe acepte un argumento más general que el método en la clase base:

clase CatShelter extiende AnimalShelter { void putAnimal ( Object animal ) { // ... } }Solo unos pocos lenguajes orientados a objetos permiten esto (por ejemplo, Python cuando se verifica con mypy ). C++, Java y la mayoría de los demás lenguajes que admiten sobrecarga y/o enmascaramiento lo interpretarían como un método con un nombre sobrecargado o enmascarado.
Sin embargo, Sather apoyó tanto la covarianza como la contravarianza. La convención de llamada para los métodos sobrescritos es covariante con parámetros de salida y valores de retorno, y contravariante con parámetros normales (con la moda en ).
Tipo de parámetro del método covariante
Un par de lenguajes de programación populares, Eiffel y Dart [ 8 ], permiten que los parámetros de un método sobrescrito tengan un tipo más específico que el método en la superclase (covarianza de tipo de parámetro). Por lo tanto, el siguiente código Dart pasaría la verificación de tipos, sobrescribiendo el método en la clase base:putAnimal

clase CatShelter extiende AnimalShelter {void putAnimal ( covariant Cat animal ) { // ... } }Esto no es seguro en cuanto a tipos. Al convertir un tipo a otro , se puede intentar colocar un perro en un refugio para gatos. Esto no cumple con las restricciones de parámetros y dará como resultado un error en tiempo de ejecución. La falta de seguridad de tipos (conocida como el "problema del gato" en la comunidad Eiffel, donde "cat" o "CAT" es una Disponibilidad o Tipo Cambiado) ha sido un problema de larga data. A lo largo de los años, se han propuesto varias combinaciones de análisis estático global, análisis estático local y nuevas características del lenguaje para remediarlo, [ 9 ] [ 10 ] y estas se han implementado en algunos compiladores Eiffel.CatShelterAnimalShelterCatShelter
A pesar del problema de seguridad de tipos, los diseñadores de Eiffel consideran que los tipos de parámetros covariantes son cruciales para modelar los requisitos del mundo real. [ 10 ] El refugio para gatos ilustra un fenómeno común: es un tipo de refugio para animales, pero tiene restricciones adicionales , y parece razonable usar herencia y tipos de parámetros restringidos para modelarlo. Al proponer este uso de la herencia, los diseñadores de Eiffel rechazan el principio de sustitución de Liskov , que establece que los objetos de las subclases siempre deben estar menos restringidos que los objetos de su superclase.
Otro ejemplo de un lenguaje de programación convencional que permite la covarianza en los parámetros de los métodos es PHP, en lo que respecta a los constructores de clases. En el siguiente ejemplo, se acepta el método __construct(), a pesar de que su parámetro es covariante con el parámetro del método padre. Si este método fuera distinto de __construct(), se produciría un error.
interfaz AnimalInterface {}Interfaz DogInterface extiende AnimalInterface {}class Dog implements DogInterface {}clase Pet { función pública __construct ( AnimalInterface $animal ) {} }clase PetDog extiende Pet { función pública __construct ( DogInterface $dog ) { padre :: __construct ( $dog ); } }Otro ejemplo donde los parámetros covariantes resultan útiles son los llamados métodos binarios, es decir, métodos donde se espera que el parámetro sea del mismo tipo que el objeto sobre el que se llama al método. Un ejemplo es el método: comprueba si un elemento precede o sigue a otro en un orden determinado, pero la forma de comparar, por ejemplo, dos números racionales será diferente a la de comparar dos cadenas de caracteres. Otros ejemplos comunes de métodos binarios incluyen pruebas de igualdad, operaciones aritméticas y operaciones de conjuntos como subconjunto y unión.compareToa.compareTo(b)ab
En versiones anteriores de Java, el método de comparación se especificaba como una interfaz :Comparable
Interfaz comparable {int compareTo ( Object o ); }El inconveniente de esto es que el método está especificado para tomar un argumento de tipo . Una implementación típica primero convertiría este argumento a un tipo inferior (lanzando un error si no es del tipo esperado):Object
class RationalNumber implements Comparable { int numerador ; int denominador ; // ... public int compareTo ( Object other ) { RationalNumber otherNum = ( RationalNumber ) other ; return Integer.compare ( numerador * otherNum.denominador , otherNum.numerador * denominador ) ; } }En un lenguaje con parámetros covariantes, el argumento podría recibir directamente el tipo deseado , ocultando la conversión de tipo. (Por supuesto, esto seguiría generando un error en tiempo de ejecución si luego se llamara, por ejemplo, en un ).compareToRationalNumbercompareToString
Evitar la necesidad de tipos de parámetros covariantes
Otras características del lenguaje pueden proporcionar los beneficios aparentes de los parámetros covariantes, al tiempo que preservan la sustituibilidad de Liskov.
En un lenguaje con genéricos (también conocido como polimorfismo paramétrico ) y cuantificación acotada , los ejemplos anteriores se pueden escribir de forma segura en cuanto a tipos. [ 11 ] En lugar de definir , definimos una clase parametrizada . (Una desventaja de esto es que quien implementa la clase base necesita prever qué tipos deberán especializarse en las subclases).AnimalShelterShelter<T>
clase Refugio < T extiende Animal > {T getAnimalForAdoption () { // ... }void putAnimal ( T animal ) { // ... } }clase CatShelter extiende Shelter < Cat > {Cat getAnimalForAdoption () { // ... }void putAnimal ( gato animal ) { // ... } }De manera similar, en versiones recientes de Java la interfaz se ha parametrizado, lo que permite omitir la conversión descendente de forma segura en cuanto a tipos:Comparable
class RationalNumber implements Comparable < RationalNumber > {int numerador ; int denominador ; // ... public int compareTo ( RationalNumber otherNum ) { return Integer.compare ( numerador * otherNum.denominador , otherNum.numerador * denominador ) ; } }Otra característica del lenguaje que puede ayudar es el despacho múltiple . Una razón por la que los métodos binarios son difíciles de escribir es que en una llamada como , seleccionar la implementación correcta de realmente depende del tipo en tiempo de ejecución de y , pero en un lenguaje OO convencional solo se tiene en cuenta el tipo en tiempo de ejecución de . En un lenguaje con despacho múltiple al estilo del Sistema de Objetos de Common Lisp (CLOS) , el método de comparación podría escribirse como una función genérica donde ambos argumentos se utilizan para la selección del método.a.compareTo(b)compareToaba
Giuseppe Castagna [ 12 ] observó que en un lenguaje tipado con despacho múltiple, una función genérica puede tener algunos parámetros que controlan el despacho y algunos parámetros "sobrantes" que no lo hacen. Debido a que la regla de selección de métodos elige el método aplicable más específico, si un método sobrescribe a otro, entonces el método sobrescritor tendrá tipos más específicos para los parámetros de control. Por otro lado, para garantizar la seguridad de tipos, el lenguaje aún debe requerir que los parámetros sobrantes sean al menos igual de generales. Usando la terminología anterior, los tipos usados para la selección de métodos en tiempo de ejecución son covariantes, mientras que los tipos no usados para la selección de métodos en tiempo de ejecución del método son contravariantes. Los lenguajes convencionales de despacho único como Java también obedecen esta regla: solo se usa un argumento para la selección de métodos (el objeto receptor, pasado a un método como argumento oculto ), y de hecho el tipo de es más especializado dentro de los métodos sobrescritores que en la superclase.thisthis
Castagna sugiere que los ejemplos donde los tipos de parámetros covariantes son superiores (en particular, los métodos binarios) deberían manejarse mediante despacho múltiple, que es inherentemente covariante. Sin embargo, la mayoría de los lenguajes de programación no admiten despacho múltiple.
Resumen de la varianza y la herencia
La siguiente tabla resume las reglas para sobrescribir métodos en los lenguajes mencionados anteriormente.
Tipos genéricos
En los lenguajes de programación que admiten genéricos (también conocidos como polimorfismo paramétrico ), el programador puede extender el sistema de tipos con nuevos constructores. Por ejemplo, una interfaz de C# como permite construir nuevos tipos como o . Surge entonces la pregunta de cuál debería ser la varianza de estos constructores de tipos.IList<T>IList<Animal>IList<Cat>
Existen dos enfoques principales. En lenguajes con anotaciones de varianza en el sitio de declaración (por ejemplo, C# ), el programador anota la definición de un tipo genérico con la varianza prevista de sus parámetros de tipo. En cambio, con anotaciones de varianza en el sitio de uso (por ejemplo, Java ), el programador anota los lugares donde se instancia un tipo genérico.
Anotaciones de variación del sitio de declaración
Los lenguajes más populares con anotaciones de varianza en el sitio de declaración son C# y Kotlin (usando las palabras clave outy in), y Scala y OCaml (usando las palabras clave +y -). C# solo permite anotaciones de varianza para tipos de interfaz, mientras que Kotlin, Scala y OCaml las permiten tanto para tipos de interfaz como para tipos de datos concretos.
Interfaces
En C#, cada parámetro de tipo de una interfaz genérica puede marcarse como covariante ( out), contravariante ( in) o invariante (sin anotación). Por ejemplo, podemos definir una interfaz de iteradores de solo lectura y declararla como covariante (out) en su parámetro de tipo.IEnumerator<T>
interface IEnumerator < out T > { T Current { get ; } bool MoveNext (); }Con esta declaración, IEnumeratorse tratará como covariante en su parámetro de tipo, por ejemplo, es un subtipo de .IEnumerator<Cat>IEnumerator<Animal>
El verificador de tipos garantiza que cada declaración de método en una interfaz solo mencione los parámetros de tipo de manera consistente con las anotaciones in/ out. Es decir, un parámetro que se declaró covariante no debe aparecer en ninguna posición contravariante (donde una posición es contravariante si aparece bajo un número impar de constructores de tipo contravariantes). La regla precisa [ 13 ] [ 14 ] es que los tipos de retorno de todos los métodos en la interfaz deben ser válidos covariantemente y todos los tipos de parámetros de método deben ser válidos contravariantemente , donde S-ly válido se define de la siguiente manera:
- Los tipos no genéricos (clases, estructuras, enumeraciones, etc.) son válidos tanto de forma covariante como contravariante.
- Un parámetro de tipo
Tes válido covariantemente si no fue marcadoin, y válido contravariantemente si no fue marcadoout. - Un tipo de matriz es S-ly válido si lo es. (Esto se debe a que C# tiene matrices covariantes).
A[]A - Un tipo genérico es S-ly válido si para cada parámetro ,
G<A1,A2,...,An>Ai- Ai es S-ly válido, y el i- ésimo parámetro
Gse declara covariante, o - Ai es válido (no S)-ly, y el i -ésimo parámetro
Gse declara contravariante, o - Ai es válido tanto covariante como contravariantemente, y el i -ésimo parámetro de
Gse declara invariante.
- Ai es S-ly válido, y el i- ésimo parámetro
Como ejemplo de cómo se aplican estas reglas, consideremos la interfaz.IList<T>
interface IList < T > { void Insert ( int index , T item ); IEnumerator < T > GetEnumerator (); }TEl tipo de parámetro Insertdebe ser válido contravariantemente, es decir, el parámetro de tipo Tno debe estar etiquetado out. De manera similar, el tipo de resultado debe ser válido covariantemente, es decir (dado que es una interfaz covariante) el tipo debe ser válido covariantemente, es decir, el parámetro de tipo no debe estar etiquetado . Esto muestra que la interfaz no puede estar marcada como covariante ni contravariante.IEnumerator<T>GetEnumeratorIEnumeratorTTinIList
En el caso común de una estructura de datos genérica como IList, estas restricciones significan que un outparámetro solo se puede usar para métodos que obtienen datos de la estructura, y un inparámetro solo se puede usar para métodos que insertan datos en la estructura, de ahí la elección de palabras clave.
Datos
C# permite anotaciones de varianza en los parámetros de las interfaces, pero no en los parámetros de las clases. Dado que los campos en las clases de C# siempre son mutables, las clases con parámetros de varianza en C# no serían muy útiles. Pero los lenguajes que enfatizan los datos inmutables pueden hacer un buen uso de los tipos de datos covariantes. Por ejemplo, en Scala , Kotlin y OCaml, el tipo de lista inmutable es covariante: es un subtipo de .List[Cat]List[Animal]
Las reglas de Scala para comprobar las anotaciones de varianza son esencialmente las mismas que las de C#. Sin embargo, existen algunas convenciones que se aplican en particular a las estructuras de datos inmutables. Estas se ilustran en el siguiente fragmento de la definición de la clase.List[A]
clase abstracta sellada List [ + A ] extiende AbstractSeq [ A ] { def cabeza : A def cola : List [ A ]/** Agrega un elemento al principio de esta lista. */ def :: [ B >: A ] ( x : B ): List [ B ] = new scala . collection . immutable . :: ( x , this ) /** ... */ }Primero, los miembros de clase que tienen un tipo variante deben ser inmutables. Aquí, headtiene el tipo A, que fue declarado covariante ( +), y de hecho headfue declarado como un método ( def). Intentar declararlo como un campo mutable ( var) sería rechazado como error de tipo.
En segundo lugar, incluso si una estructura de datos es inmutable, a menudo tendrá métodos donde el tipo de parámetro aparece de forma contravariante. Por ejemplo, consideremos el método ::que agrega un elemento al principio de una lista. (La implementación funciona creando un nuevo objeto de la clase:: de nombre similar , la clase de listas no vacías). El tipo más obvio que se le podría dar sería
def :: ( x : A ): Lista [ A ]Sin embargo, esto sería un error de tipo, porque el parámetro covariante Aaparece en una posición contravariante (como parámetro de función). Pero hay un truco para evitar este problema. Damos ::un tipo más general, que permite agregar un elemento de cualquier tipo B siempre que Bsea un supertipo de A. Nótese que esto depende de Listser covariante, ya que this tiene tipo y lo tratamos como si tuviera tipo . A primera vista puede que no sea obvio que el tipo generalizado sea correcto, pero si el programador comienza con la declaración de tipo más simple, los errores de tipo señalarán el lugar que necesita ser generalizado.List[A]List[B]
Inferencia de la varianza
Es posible diseñar un sistema de tipos donde el compilador infiera automáticamente las mejores anotaciones de varianza posibles para todos los parámetros de tipo de datos. [ 15 ] Sin embargo, el análisis puede volverse complejo por varias razones. Primero, el análisis es no local, ya que la varianza de una interfaz depende de la varianza de todas las interfaces que menciona. Segundo, para obtener soluciones óptimas únicas, el sistema de tipos debe permitir parámetros bivariantes (que son simultáneamente covariantes y contravariantes). Y finalmente, la varianza de los parámetros de tipo debería ser, sin duda, una elección deliberada del diseñador de una interfaz, no algo que simplemente sucede.II
Por estas razones [ 16 ], la mayoría de los lenguajes realizan muy poca inferencia de varianza. C# y Scala no infieren ninguna anotación de varianza. OCaml puede inferir la varianza de tipos de datos concretos parametrizados, pero el programador debe especificar explícitamente la varianza de tipos abstractos (interfaces).
Por ejemplo, consideremos un tipo de dato de OCaml Tque encapsula una función.
tipo ( ' a , ' b ) t = T de ( ' a -> ' b )El compilador inferirá automáticamente que Tes contravariante en el primer parámetro y covariante en el segundo. El programador también puede proporcionar anotaciones explícitas, que el compilador verificará que se cumplan. Por lo tanto, la siguiente declaración es equivalente a la anterior:
tipo (- ' a , + ' b ) t = T de ( ' a -> ' b )En OCaml, las anotaciones explícitas resultan útiles al especificar interfaces. Por ejemplo, la interfaz de la biblioteca estándar para tablas de asociación incluye una anotación que indica que el constructor del tipo de mapa es covariante en el tipo de resultado.Map.S
tipo de módulo S = tipo de sig tipo de clave (+ ' a ) t val vacío : ' a t val mem : clave -> ' a t -> bool ... finEsto garantiza que, por ejemplo, sea un subtipo de .catIntMap.tanimalIntMap.t
Anotaciones de variación del sitio de uso (comodines)
Una desventaja del enfoque del sitio de declaración es que muchos tipos de interfaz deben hacerse invariantes. Por ejemplo, vimos anteriormente que IListdebía ser invariante, porque contenía tanto Insertcomo GetEnumerator. Para exponer más varianza, el diseñador de la API podría proporcionar interfaces adicionales que proporcionen subconjuntos de los métodos disponibles (por ejemplo, una "lista de solo inserción" que solo proporcione Insert). Sin embargo, esto se vuelve rápidamente difícil de manejar.
La variación en el sitio de uso implica que la variación deseada se indica mediante una anotación en el sitio específico del código donde se utilizará el tipo. Esto brinda a los usuarios de una clase más oportunidades para la subtipificación sin que el diseñador de la clase tenga que definir múltiples interfaces con diferentes variaciones. En cambio, al instanciar un tipo genérico como un tipo parametrizado, el programador puede indicar que solo se utilizará un subconjunto de sus métodos. En efecto, cada definición de una clase genérica también pone a disposición interfaces para las partes covariantes y contravariantes de dicha clase.
Java proporciona anotaciones de varianza de sitio de uso a través de comodines , una forma restringida de tipos existenciales acotados . Un tipo parametrizado puede instanciarse mediante un comodín junto con un límite superior o inferior, por ejemplo, o . Un comodín sin límite como es equivalente a . Dicho tipo representa para algún tipo desconocido que satisface el límite. [ 3 ] : 139 Por ejemplo, si tiene el tipo , entonces el verificador de tipos aceptará?List<?extendsAnimal>List<?superAnimal>List<?>List<?extendsObject>List<X>XlList<?extendsAnimal>
Animal a = l . obtener ( 3 );porque se sabe que el tipo es un subtipo de , peroXAnimal
l.add ( nuevo Animal ( ) );será rechazado como un error de tipo ya que un no es necesariamente un . En general, dada alguna interfaz , una referencia a un prohíbe usar métodos de la interfaz donde ocurre de forma contravariante en el tipo del método. Por el contrario, si tuviera tipo se podría llamar pero no .AnimalXI<T>I<?extendsT>TlList<?superAnimal>l.addl.get

Si bien los tipos parametrizados sin comodines en Java son invariantes (por ejemplo, no existe una relación de subtipo entre y ), los tipos con comodines pueden hacerse más específicos al especificar un límite más ajustado. Por ejemplo, es un subtipo de . Esto demuestra que los tipos con comodines son covariantes en sus límites superiores (y también contravariantes en sus límites inferiores ). En total, dado un tipo con comodines como , hay tres maneras de formar un subtipo: especializando la clase , especificando un límite más ajustado , o reemplazando el comodín con un tipo específico (véase la figura). [ 3 ] : 139List<Cat>List<Animal>List<?extendsCat>List<?extendsAnimal>C<?extendsT>CT?
Al aplicar dos de las tres formas de subtipado anteriores, es posible, por ejemplo, pasar un argumento de tipo a un método que espera un . Este es el tipo de expresividad que resulta de los tipos de interfaz covariantes. El tipo actúa como un tipo de interfaz que contiene solo los métodos covariantes de , pero el implementador de no tuvo que definirlo de antemano.List<Cat>List<?extendsAnimal>List<?extendsAnimal>List<T>List<T>
En el caso común de una estructura de datos genérica IList, se utilizan parámetros covariantes para los métodos que extraen datos de la estructura y parámetros contravariantes para los métodos que los insertan. El acrónimo PECS (Productor Extiende, Consumidor Super), del libro Effective Java de Joshua Bloch, facilita recordar cuándo usar covarianza y contravarianza. [ 3 ] : 141
Los comodines son flexibles, pero tienen un inconveniente. Si bien la varianza del sitio de uso significa que los diseñadores de API no necesitan considerar la varianza de los parámetros de tipo para las interfaces, a menudo deben usar firmas de métodos más complicadas. Un ejemplo común involucra la Comparableinterfaz. [ 3 ] : 66 Supongamos que queremos escribir una función que encuentre el elemento más grande en una colección. Los elementos deben implementar el método, [ 3 ] : 66 por lo que un primer intento podría sercompareTo
< T extiende Comparable < T >> T max ( Colección < T > coll );Sin embargo, este tipo no es lo suficientemente general : se puede encontrar el máximo de un , pero no de un . El problema es que no implementa , sino la interfaz (mejor) . En Java, a diferencia de C#, no se considera un subtipo de . En su lugar, el tipo de debe modificarse:Collection<Calendar>Collection<GregorianCalendar>GregorianCalendarComparable<GregorianCalendar>Comparable<Calendar>Comparable<Calendar>Comparable<GregorianCalendar>max
< T extiende Comparable <? super T >> T max ( Colección < T > coll );El comodín acotado transmite la información de que solo se llaman métodos contravariantes de la interfaz. Este ejemplo en particular es frustrante porque todos los métodos son contravariantes, por lo que esa condición es trivialmente verdadera. Un sistema en el sitio de declaración podría manejar este ejemplo con menos desorden anotando solo la definición de .?superTmaxComparableComparableComparable
El método se puede modificar aún más utilizando un comodín con límite superior para el parámetro del método: [ 17 ]max
< T extiende Comparable <? super T >> T max ( Colección <? extiende T > coll );Comparación de las anotaciones en el sitio de declaración y en el sitio de uso
Las anotaciones de variación en el sitio de uso brindan mayor flexibilidad, lo que permite que más programas realicen comprobaciones de tipos. Sin embargo, han sido criticadas por la complejidad que añaden al lenguaje, lo que da lugar a firmas de tipo y mensajes de error complicados.
Una forma de evaluar si la flexibilidad adicional resulta útil es observar si se utiliza en programas existentes. Un estudio de un amplio conjunto de bibliotecas Java [ 15 ] reveló que el 39 % de las anotaciones comodín podrían haberse reemplazado directamente por anotaciones en el sitio de declaración. Por lo tanto, el 61 % restante indica los casos en los que Java se beneficia al disponer del sistema de anotaciones en el sitio de uso.
En un lenguaje con declaración de dominio, las bibliotecas deben exponer menos varianza o definir más interfaces. Por ejemplo, la biblioteca Scala Collections define tres interfaces separadas para clases que emplean covarianza: una interfaz base covariante que contiene métodos comunes, una versión mutable invariante que agrega métodos con efectos secundarios y una versión inmutable covariante que puede especializar las implementaciones heredadas para explotar el uso compartido de estructuras. [ 18 ] Este diseño funciona bien con anotaciones de declaración de dominio, pero la gran cantidad de interfaces conlleva un costo de complejidad para los clientes de la biblioteca. Y modificar la interfaz de la biblioteca puede no ser una opción ; en particular, uno de los objetivos al agregar genéricos a Java fue mantener la retrocompatibilidad binaria.
Por otro lado, los comodines de Java son complejos en sí mismos. En una presentación en una conferencia [ 19 ] , Joshua Bloch los criticó por ser demasiado difíciles de entender y usar, afirmando que al agregar soporte para cierres , "simplemente no podemos permitirnos más comodines ". Las primeras versiones de Scala usaban anotaciones de varianza en el sitio de uso, pero los programadores las encontraron difíciles de usar en la práctica, mientras que las anotaciones en el sitio de declaración resultaron muy útiles al diseñar clases. [ 20 ] Las versiones posteriores de Scala agregaron tipos existenciales y comodines al estilo de Java; sin embargo, según Martin Odersky , si no hubiera habido necesidad de interoperabilidad con Java, probablemente no se habrían incluido. [ 21 ]
Ross Tate argumenta [ 22 ] que parte de la complejidad de los comodines de Java se debe a la decisión de codificar la varianza del sitio de uso utilizando una forma de tipos existenciales. Las propuestas originales [ 23 ] [ 24 ] utilizaron una sintaxis de propósito especial para las anotaciones de varianza, escribiendo en lugar de la más verbosa de Java .List<+Animal>List<?extendsAnimal>
Dado que los comodines son una forma de tipos existenciales, pueden usarse para más cosas que solo varianza. Un tipo como ("una lista de tipo desconocido" [ 25 ] ) permite pasar objetos a métodos o almacenarlos en campos sin especificar exactamente sus parámetros de tipo. Esto es particularmente valioso para clases como donde la mayoría de los métodos no mencionan el parámetro de tipo.List<?>Class
Sin embargo, la inferencia de tipos para tipos existenciales es un problema difícil. Para el implementador del compilador, los comodines de Java plantean problemas con la terminación del verificador de tipos, la inferencia de argumentos de tipo y los programas ambiguos. [ 26 ] En general, es indecidible si un programa Java que usa genéricos está bien tipado o no, [ 27 ] por lo que cualquier verificador de tipos tendrá que entrar en un bucle infinito o agotar el tiempo de espera para algunos programas. Para el programador, esto conduce a mensajes de error de tipo complicados. Java verifica los tipos comodín reemplazando los comodines con nuevas variables de tipo (la llamada conversión de captura ). Esto puede hacer que los mensajes de error sean más difíciles de leer, porque se refieren a variables de tipo que el programador no escribió directamente. Por ejemplo, intentar agregar un a un dará un error comoCatList<?extendsAnimal>
El método List.add (captura#1) no es aplicable. (El argumento real Cat no se puede convertir a capture#1 mediante la conversión de invocación de método) donde capture#1 es una variable de tipo nueva: captura#1 extiende Animal desde captura de ? extiende Animal
Dado que tanto las anotaciones en el sitio de declaración como en el sitio de uso pueden ser útiles, algunos sistemas de tipos proporcionan ambas. [ 15 ] [ 22 ]
Véase también
Notas
- ↑ Esto solo ocurre en un caso patológico. Por ejemplo,
I<T> = int: se puede poner cualquier tipo enTy el resultado sigue siendoint.
Referencias
- 1 2 3 Skeet, Jon (23 de marzo de 2019). C# en profundidad . Manning. ISBN 978-1617294532.
- ↑ Delegado Func<T, TResult> - Documentación de MSDN
- 1 2 3 4 5 6 7 8 Bloch, Joshua (2018). "Java eficaz: Guía del lenguaje de programación" (tercera ed.). Addison-Wesley. ISBN 978-0134685991.
- ↑ Reynolds, John C. (1981). La esencia de Algol . Simposio sobre lenguajes algorítmicos. North-Holland.
- ↑ Cardelli, Luca (1984). Una semántica de la herencia múltiple (PDF) . Semántica de los tipos de datos (Simposio Internacional Sophia-Antipolis, Francia, 27-29 de junio de 1984). Lecture Notes in Computer Science. Vol. 173. Springer. pp. 51-67 . doi : 10.1007/3-540-13346-1_2 . ISBN 3-540-13346-1.Versión extendida: — (febrero de 1988). "Una semántica de la herencia múltiple". Information and Computation . 76 (2/3): 138– 164. CiteSeerX 10.1.1.116.1298 . doi : 10.1016/0890-5401(88)90007-7 .
- ↑ Torgersen, Mads. "C# 9.0 en el registro" .
- ↑ Allison, Chuck. "¿Qué hay de nuevo en C++ estándar?" .
- ↑ "Corrección de problemas comunes de tipos" . Lenguaje de programación Dart .
- ↑ Bertrand Meyer (octubre de 1995). "Tipado estático" (PDF) . OOPSLA 95 (Programación orientada a objetos, sistemas, lenguajes y aplicaciones), Atlanta, 1995 .
- 1 2 Howard, Mark; Bezault, Eric; Meyer, Bertrand; Colnet, Dominique; Stapf, Emmanuel; Arnout, Karine; Keller, Markus (abril de 2003). "Covarianza segura en cuanto a tipos: los compiladores competentes pueden detectar todos los errores" (PDF) . Recuperado el 23 de mayo de 2013 .
- ↑ Franz Weber (1992). "Obtener la equivalencia entre la corrección de clases y la corrección del sistema: cómo obtener la covarianza correcta". TOOLS 8 (8.ª conferencia sobre tecnología de lenguajes y sistemas orientados a objetos), Dortmund, 1992. CiteSeerX 10.1.1.52.7872 .
- ↑ Castagna, Giuseppe (mayo de 1995). "Covarianza y contravarianza: conflicto sin causa". ACM Transactions on Programming Languages and Systems . 17 (3): 431– 447. CiteSeerX 10.1.1.115.5992 . doi : 10.1145/203095.203096 . S2CID 15402223 .
- ↑ Lippert, Eric (3 de diciembre de 2009). "Reglas exactas para la validez de la varianza" . Recuperado el 16 de agosto de 2016 .
- ↑ "Sección II.9.7". Norma internacional ECMA ECMA-335 Infraestructura de lenguaje común (CLI) (6.ª ed.). Junio de 2012.
- 1 2 3 Altidor, John; Shan, Huang Shan; Smaragdakis, Yannis (2011). "Domando los comodines: combinando la varianza del sitio de definición y del sitio de uso". Actas de la 32.ª conferencia ACM SIGPLAN sobre diseño e implementación de lenguajes de programación (PLDI'11) . ACM. págs. 602–613 . CiteSeerX 10.1.1.225.8265 . doi : 10.1145/1993316.1993569 . ISBN 9781450306638.
- ↑ Lippert, Eric (29 de octubre de 2007). "Covarianza y contravarianza en C# Parte siete: ¿Por qué necesitamos una sintaxis?" . Recuperado el 16 de agosto de 2016 .
- ↑ Bloch 2018 , págs. 139–145, Capítulo §5 Elemento 31: Utilice comodines limitados para aumentar la flexibilidad de la API.
- ↑ Odersky, Marin; Spoon, Lex (7 de septiembre de 2010). "La API de colecciones de Scala 2.8" . Recuperado el 16 de agosto de 2016 .
- ↑ Bloch, Joshua (noviembre de 2007). "La controversia de los cierres [ video ] " . Presentación en Javapolis'07. Archivado del original el 2 de febrero de 2014.
{{cite web}}: CS1 mantenimiento: ubicación ( enlace ) - ↑ Odersky, Martin; Zenger, Matthias (2005). «Abstracciones de componentes escalables» (PDF) . Actas de la 20.ª conferencia anual ACM SIGPLAN sobre programación orientada a objetos, sistemas, lenguajes y aplicaciones (OOPSLA '05) . ACM. págs. 41–57 . CiteSeerX 10.1.1.176.5313 . doi : 10.1145/1094811.1094815 . ISBN 1595930310.
- ↑ Venners, Bill; Sommers, Frank (18 de mayo de 2009). "El propósito del sistema de tipos de Scala: una conversación con Martin Odersky, parte III" . Recuperado el 16 de agosto de 2016 .
- 1 2 Tate, Ross (2013). "Varianza de sitios mixtos" . FOOL '13: Actas informales del 20.º Taller internacional sobre fundamentos de lenguajes orientados a objetos . CiteSeerX 10.1.1.353.4691 .
- ↑ Igarashi, Atsushi; Viroli, Mirko (2002). "Sobre la subtipificación basada en la varianza para tipos paramétricos". Actas de la 16.ª Conferencia Europea sobre Programación Orientada a Objetos (ECOOP '02) . Lecture Notes in Computer Science. Vol. 2374. pp. 441–469 . CiteSeerX 10.1.1.66.450 . doi : 10.1007/3-540-47993-7_19 . ISBN 3-540-47993-7.
- ↑ Thorup, Kresten Krab; Torgersen, Mads (1999). "Unifying Genericity: Combining the Benefits of Virtual Types and Parameterized Classes". Object -Oriented Programming (ECOOP '99) . Lecture Notes in Computer Science. Vol. 1628. Springer. pp. 186–204 . CiteSeerX 10.1.1.91.9795 . doi : 10.1007/3-540-48743-3_9 . ISBN 3-540-48743-3.
- ↑ "Tutoriales de Java™, Genéricos (Actualizado), Comodines sin límites" . Consultado el 17 de julio de 2020 .
- ↑ Tate, Ross; Leung, Alan; Lerner, Sorin (2011). "Dominando los comodines en el sistema de tipos de Java" . Actas de la 32.ª conferencia ACM SIGPLAN sobre diseño e implementación de lenguajes de programación (PLDI '11) . págs. 614–627 . CiteSeerX 10.1.1.739.5439 . ISBN 9781450306638.
- ↑ Grigore, Radu (2017). "Los genéricos de Java son Turing completos". Actas del 44.º Simposio ACM SIGPLAN sobre Principios de Lenguajes de Programación (POPL'17) . págs. 73–85 . arXiv : 1605.05274 . Bibcode : 2016arXiv160505274G . ISBN 9781450346603.
Enlaces externos
- Covarianza y contravarianza: Fabulosas aventuras en la programación : Una serie de artículos sobre las dificultades de implementación relacionadas con la covarianza y la contravarianza en C#, por Eric Lippert. (Archivado en 2008 desde MSDN . Algunas publicaciones aún están disponibles en su sitio web personal . Más publicaciones en este archivo desde 2019 ).
- Contravarianza vs. Covarianza (tenga en cuenta que este artículo no está actualizado con respecto a C++)
- Cierres para el lenguaje de programación Java 7 (v0.5)
- La teoría detrás de la covarianza y la contravarianza en C# 4
- Programación orientada a objetos
- teoría de tipos
- Polimorfismo (informática)
- Comparación de lenguajes de programación