PHP 8.3.21 Released!

Covarianza y Contravarianza

A partir de PHP 7.2.0, se introdujo la contravarianza parcial eliminando las restricciones de tipo en los parámetros de un método hijo. A partir de PHP 7.4.0, se añadieron la covarianza y la contravarianza completas.

La covarianza permite a un método hijo devolver un tipo más específico que el tipo de retorno de su método padre. La contravarianza permite que un tipo de parámetro sea menos específico en un método hijo que en el de la clase padre.

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

Un tipo de clase se considera menos específico si lo contrario es cierto.

Covarianza

Para ilustrar el funcionamiento de la covarianza, se crea una simple clase padre abstracta, Animal que será extendida por clases hijas, 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";
}
}

Téngase en cuenta que no hay métodos que devuelvan valores en este ejemplo. Se añadirán algunas fábricas y devolverán un nuevo objeto de clase de tipo Animal, Cat, o Dog.

<?php

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

class
CatShelter implements AnimalShelter
{
public function
adopt(string $name): Cat // en lugar de devolver el tipo de clase Animal, puede devolver el tipo de clase Cat
{
return new
Cat($name);
}
}

class
DogShelter implements AnimalShelter
{
public function
adopt(string $name): Dog // en lugar de devolver el tipo de clase Animal, puede devolver el tipo de clase 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

Retomando el ejemplo anterior con las clases Animal, Cat y Dog, se incluyen dos clases llamadas Food y AnimalFood, y se añade un método eat(AnimalFood $food) 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 contravarianza, el método método eat es sobrecargado en la clase Dog para permitir cualquier objeto de tipo 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 muestra 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é sucede si $kitty intenta comer (eat()) la banana ($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

Variación de tipo de las propiedades

Por defecto, las propiedades no son ni covariantes ni contravariantes, por lo tanto, son invariantes. En otras palabras, su tipo no puede cambiar en absoluto en una clase hija. La razón es que las operaciones "get" deben ser covariantes, y las operaciones "set" deben ser contravariantes. La única manera para que una propiedad cumpla con estos dos requisitos es ser invariante.

A partir de PHP 8.4.0, con la adición de las propiedades abstractas (en una interfaz o una clase abstracta) y propiedades virtuales, es posible declarar una propiedad que solo tenga una operación "get" o "set". En consecuencia, las propiedades abstractas o las propiedades virtuales que solo requieren la operación "get" pueden ser covariantes. De manera similar, una propiedad abstracta o una propiedad virtual que solo requiere la operación "set" puede ser contravariante.

Sin embargo, una vez que una propiedad tiene tanto una operación "get" como "set", ya no es covariante ni contravariante para una extensión futura. En otras palabras, se vuelve invariante.

Ejemplo #1 Variación del tipo de las propiedades

<?php
class Animal {}
class
Dog extends Animal {}
class
Poodle extends Dog {}

interface
PetOwner
{
// Solo se requiere la operación "get", por lo tanto, puede ser covariante.
public Animal $pet { get; }
}

class
DogOwner implements PetOwner
{
// Puede ser un tipo más restrictivo, ya que el lado "get"
// siempre devuelve un Animal. Sin embargo, como propiedad nativa,
// los hijos de esta clase ya no pueden cambiar el tipo.
public Dog $pet;
}

class
PoodleOwner extends DogOwner
{
// ESTO NO ESTÁ PERMITIDO, ya que DogOwner::$pet tiene tanto
// las operaciones "get" como "set" definidas y requeridas.
public Poodle $pet;
}
?>
add a note

User Contributed Notes 3 notes

up
96
xedin dot unknown at gmail dot com
5 years ago
I would like to explain why covariance and contravariance are important, and why they apply to return types and parameter types respectively, and not the other way around.

Covariance is probably easiest to understand, and is directly related to the Liskov Substitution Principle. Using the above example, let's say that we receive an `AnimalShelter` object, and then we want to use it by invoking its `adopt()` method. We know that it returns an `Animal` object, and no matter what exactly that object is, i.e. whether it is a `Cat` or a `Dog`, we can treat them the same. Therefore, it is OK to specialize the return type: we know at least the common interface of any thing that can be returned, and we can treat all of those values in the same way.

Contravariance is slightly more complicated. It is related very much to the practicality of increasing the flexibility of a method. Using the above example again, perhaps the "base" method `eat()` accepts a specific type of food; however, a _particular_ animal may want to support a _wider range_ of food types. Maybe it, like in the above example, adds functionality to the original method that allows it to consume _any_ kind of food, not just that meant for animals. The "base" method in `Animal` already implements the functionality allowing it to consume food specialized for animals. The overriding method in the `Dog` class can check if the parameter is of type `AnimalFood`, and simply invoke `parent::eat($food)`. If the parameter is _not_ of the specialized type, it can perform additional or even completely different processing of that parameter - without breaking the original signature, because it _still_ handles the specialized type, but also more. That's why it is also related closely to the Liskov Substitution: consumers may still pass a specialized food type to the `Animal` without knowing exactly whether it is a `Cat` or `Dog`.
up
8
Hayley Watson
2 years ago
The gist of how the Liskov Substition Princple applies to class types is, basically: "If an object is an instance of something, it should be possible to use it wherever an instance of something is allowed". The Co- and Contravariance rules come from this expectation when you remember that "something" could be a parent class of the object.

For the Cat/Animal example of the text, Cats are Animals, so it should be possible for Cats to go anywhere Animals can go. The variance rules formalise this.

Covariance: A subclass can override a method in the parent class with one that has a narrower return type. (Return values can be more specific in more specific subclasses; they "vary in the same direction", hence "covariant").
If an object has a method you expect to produce Animals, you should be able to replace it with an object where that method produces only Cats. You'll only get Cats from it but Cats are Animals, which are what you expected from the object.

Contravariance: A subclass can override a method in the parent class with one that has a parameter with a wider type. (Parameters can be less specific in more specific subclasses; they "vary in the opposite direction", hence "contravariant").
If an object has a method you expect to take Cats, you should be able to replace it with an object where that method takes any sort of Animal. You'll only be giving it Cats but Cats are Animals, which are what the object expected from you.

So, if your code is working with an object of a certain class, and it's given an instance of a subclass to work with, it shouldn't cause any trouble:
It might accept any sort of Animal where you're only giving it Cats, or it might only return Cats when you're happy to receive any sort of Animal, but LSP says "so what? Cats are Animals so you should both be satisfied."
up
11
Anonymous
5 years ago
Covariance also works with general type-hinting, note also the interface:

interface xInterface
{
public function y() : object;
}

abstract class x implements xInterface
{
abstract public function y() : object;
}

class a extends x
{
public function y() : \DateTime
{
return new \DateTime("now");
}
}

$a = new a;
echo '<pre>';
var_dump($a->y());
echo '</pre>';
To Top