La covarianza y la contravarianza son términos que describen cómo un lenguaje de programación maneja los subtipos. La varianza de un tipo determina si sus subtipos se pueden usar indistintamente con él.
La varianza es un concepto que puede parecer opaco hasta que se proporcione un ejemplo concreto. Consideremos un tipo básico Animal
con un subtipo de Dog
.
interface Animal { public function walk() : void; } interface Dog extends Animal { public function bark() : void; }
Todos los «animales» pueden caminar, pero sólo los «perros» pueden ladrar. Consideremos ahora qué sucede cuando se usa esta jerarquía de objetos en nuestra aplicación.
Interfaces de cableado juntas
CONTENIDOS DE LA PAGINA
Desde cada uno Animal
podemos caminar, podemos crear una interfaz genérica que ejercite cualquier Animal
.
interface AnimalController { public function exercise(Animal $Animal) : void; }
los AnimalController
tiene un exercise()
método que indica el tipo de archivo Animal
interfaz.
interface DogRepository { public function getById(int $id) : Dog; }
Ahora tenemos un archivo DogRepository
con un método que garantiza la devolución de un archivo Dog
.
¿Qué sucede si intentamos usar este valor con la extensión AnimalController
?
$AnimalController -> exercise($DogRepository -> getById(1));
Esto está permitido en idiomas donde se admiten parámetros covariantes. AnimalController
debe recibir un Animal.
Lo que estamos atravesando es en realidad un archivo Dog,
pero aun así satisface el Animal
contratar.
Este tipo de relación es especialmente importante a la hora de extender lecciones. Puede que queramos un genérico AnimalRepository
que recupera cualquier animal sin los detalles de su especie.
interface AnimalRepository { public function getById(int $id) : Animal; } interface DogRepository extends AnimalRepository { public function getById(int $id) : Dog; }
DogRepository
modifica el contrato de AnimalRepository
– ya que las personas que llaman recibirán un Dog
en lugar de un archivo Animal
– Pero eso no cambia radicalmente. Es solo cuestión de ser más específico sobre su tipo de retorno. A Dog
sigue siendo un Animal.
Los tipos son covariantes, por lo tanto DogRepository
La definición de es aceptable.
Mirando la contravarianza
Consideremos ahora el ejemplo inverso. Puede ser deseable tener un DogController,
que altera la forma en que se ejercitan los «perros». Lógicamente, esto aún podría extender el AnimalController
interfaz. Sin embargo, en la práctica, la mayoría de los idiomas no le permitirán anular exercise()
de la manera necesaria.
interface AnimalController { public function exercise(Animal $Animal) : void; } interface DogController extends AnimalController { public function exercise(Dog $Dog) : void; }
En este ejemplo, DogController
lo especificó exercise()
acepta solo un Dog
. Esto entra en conflicto con la definición anterior en AnimalController
, que permite el paso de cualquier «Animal». Para cumplir el contrato, DogController
debe, por tanto, aceptar también cualquier Animal
.
A primera vista, esto puede parecer confuso e innecesario. El razonamiento detrás de esta restricción se vuelve más claro cuando se menciona contra AnimalController
:
function exerciseAnimal( AnimalController $AnimalController, AnimalRepository $AnimalRepository, int $id) : void { $AnimalController -> exercise($AnimalRepository -> getById($id)); }
El problema es ese AnimalController
podría ser un archivo AnimalController
oa DogController
—Nuestro método es no saber qué implementación de interfaz está utilizando. Esto se debe a las mismas reglas de covarianza que fueron útiles anteriormente.
Me gusta AnimalController
pudo ser un DogController
, ahora hay un error de ejecución grave esperando ser descubierto. AnimalRepository
siempre devuelve un Animal
, Así que si $AnimalController
es a DogController
, la aplicación se bloqueará. los Animal
el tipo es demasiado vago para pasar al archivo DogController
exercise()
método.
Vale la pena señalar que se aceptarían los idiomas que admitan la sobrecarga de métodos DogController
. Overhead le permite definir varios métodos con el mismo nombre, siempre que tengan varios firmas (Tienen diferentes parámetros y / o tipos de retorno). DogController
tendría un extra exercise()
método que acepta sólo «Perros». Sin embargo, también debería implementar la firma ascendente que acepta cualquier «Animal».
Manejo de problemas de varianza
Todo lo anterior se puede resumir diciendo que los tipos devueltos por las funciones pueden ser covariante mientras que los tipos de argumentos deben ser contravariante. Esto significa que una función puede devolver un tipo más específico que el definido por la interfaz. También puede tomar un tipo más abstracto como argumento (aunque la mayoría de los lenguajes de programación populares no lo implementan).
Muy a menudo, se encuentran problemas de variación cuando se trabaja con genéricos y colecciones. En estos escenarios, a menudo desea un AnimalCollection
está en DogCollection
. Ellos deberían DogCollection
ampliar AnimalCollection
?
Así es como podrían verse estas interfaces:
interface AnimalCollection { public function add(Animal $a) : void; public function getById(int $id) : Animal; } interface DogCollection extends AnimalCollection { public function add(Dog $d) : void; public function getById(int $id) : Dog; }
Mirando primero getById()
, Dog
es un subtipo de Animal
. Los tipos son covariantes y se permiten los tipos devueltos covariantes. Esto es aceptable. Veamos de nuevo el problema de la varianza con add()
aunque-DogCollection
debe permitir cualquier Animal
para ser agregado para satisfacer el AnimalCollection
contratar.
Este problema generalmente se resuelve mejor haciendo que las colecciones sean inmutables. Solo permite agregar nuevos elementos en el constructor de la colección. Luego puede eliminar el archivo add()
método del conjunto, fabricación AnimalCollection
un buen candidato para DogCollection
para heredar.
Otras formas de varianza
Además de la covarianza y la contravarianza, también puede encontrar los siguientes términos:
- Bivariante: Un sistema de tipos es bivariante si tanto la covarianza como la contravarianza se aplican simultáneamente a una relación de tipos. TypeScript utilizó la bivariancia para sus parámetros antes de TypeScript 2.6
- Variante: Los tipos son variantes si se aplica covarianza o contravarianza.
- Invariante: Todo tipo que no sea variantes.
Por lo general, trabajará con tipos covariantes o contravariantes. En términos de herencia de clases, un tipo B es covariante con un tipo A si es se extiende A. Un tipo B es contravariante con un tipo A si es el antepasado de B.
Conclusión
La varianza es un concepto que explica los límites dentro de los sistemas de tipos. Por lo general, es suficiente recordar que la covarianza se acepta en los tipos de retorno, mientras que la contravarianza se usa para los parámetros.
Las reglas de varianza se derivan del principio de sustitución de Liskov. Esto indica que debería poder reemplazar instancias de una clase con instancias de sus subclases sin afectar ninguna de las propiedades del sistema más grande. Esto significa que si el tipo B extiende el tipo A, las instancias de A
se puede reemplazar con instancias de B
.
Usar nuestro ejemplo anterior significa que debemos poder reemplazar Animal
con Dog
, o AnimalController
con DogController
. Aquí vemos la razón de nuevo DogController
no se puede sobrescribir exercise()
aceptar solo perros: ya no podríamos reemplazar AnimalController
con DogController
, como consumidores que actualmente superan un Animal
ahora debería proporcionar un archivo Dog
en vez de. La covarianza y la contravarianza dictan LSP y aseguran estándares consistentes de comportamiento.