Covarianza y Contravarianza

En PHP 7.2.0, se introdujo la contravarianza parcial mediante la eliminación de las restricciones de tipo en los parámetros de un método hijo. A partir de PHP 7.4.0, se añadió soporte completo de covarianza y contravarianza.

La covarianza permite que el método hijo devuelva un tipo más específico que el tipo de devolución del método de su padre. Mientras que la contravarianza permite que un tipo de parámetro sea menos específico en un método hijo, que el de su padre.

Una declaración de tipo covarianza se considera más específica en el siguiente caso:

Una clase de tipo se considera menos específica si ocurre lo contrario.

Covarianza

Para ilustrar cómo funciona la covarianza, una simple clase de padre abstracta, Animal es creada. Animal se extenderá a las clases hijo, Cat, y Dog.

<?php

abstract class Animal
{
protected
string $name;

public function
__construct(string $name)
{
$this->name = $name;
}

abstract public function
speak();
}

class
Dog extends Animal
{
public function
speak()
{
echo
$this->name . " barks";
}
}

class
Cat extends Animal
{
public function
speak()
{
echo
$this->name . " meows";
}
}

Observe que no hay ningún método que devuelva valores en este ejemplo. Unas pocas fábricas que devuelven un nuevo objeto de tipo clase Animal, Cat, o Dog.

<?php

interface AnimalShelter
{
public function
adopt(string $name): Animal;
}

class
CatShelter implements AnimalShelter
{
public function
adopt(string $name): Cat // instead of returning class type Animal, it can return class type Cat
{
return new
Cat($name);
}
}

class
DogShelter implements AnimalShelter
{
public function
adopt(string $name): Dog // instead of returning class type Animal, it can return class type Dog
{
return new
Dog($name);
}
}

$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo
"\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();

El resultado del ejemplo sería:

 Ricky meows Mavrick barks 

Contravarianza

Continuando con el ejemplo anterior con las clases Animal, Cat, y Dog, una clase llamada Food y AnimalFood serán incluidas, y un método eat(AnimalFood $food) es añadido a la clase abstracta Animal.

<?php

class Food {}

class
AnimalFood extends Food {}

abstract class
Animal
{
protected
string $name;

public function
__construct(string $name)
{
$this->name = $name;
}

public function
eat(AnimalFood $food)
{
echo
$this->name . " eats " . get_class($food);
}
}

Para ver el comportamiento de la contravención, el método eat se sobrescribe en la clase Dog para permitir cualquier tipo de objeto Food. La clase Cat permanece sin cambios.

<?php

class Dog extends Animal
{
public function
eat(Food $food) {
echo
$this->name . " eats " . get_class($food);
}
}

El siguiente ejemplo mostrará el comportamiento de la contravarianza.

<?php

$kitty
= (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo
"\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);

El resultado del ejemplo sería:

 Ricky eats AnimalFood Mavrick eats Food 

¿Pero qué pasa si el (gatito) $kitty trata de (comer) eat la $banana?

$kitty->eat($banana);

El resultado del ejemplo sería:

 Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given 
To Top