Covariância e Contravariância
No 7.2.0, a contravariância parcial foi introduzida removendo as restrições de tipo nos parâmetros de um método filho. A partir do PHP 7.4.0, foi adicionado suporte a covariância e contravariância completas.
Covariância permite que um método filho retorne um tipo mais específico que o tipo de retorno de seu método pai. Enquanto que a contravariância permite a um parâmetro ter um tipo menos específico em um método filho, em relação ao método pai.
Uma declaração de tipo é considerada mais específica nos seguintes casos:
- Um tipo é removido de um tipo union
- Um tipo é adicionado a um tipo intersection
- Um tipo de classe é modificado a um tipo de classe filha
- iterable é modificado para array ou Traversable
Covariância
Para ilustrar como uma variância funciona, uma classe pai abstrata simples, Animal é criada. Animal será estendida a classes filhas, Cat e 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";
}
}
Note que não há nenhum método que retorne valores neste exemplo. Algumas factories serão adicionadas para retornar um novo objeto das classes Animal, Cat or Dog.
<?php
interface AnimalShelter
{
public function adopt(string $name): Animal;
}
class CatShelter implements AnimalShelter
{
public function adopt(string $name): Cat // em vez de retornar o tipo Animal, pode retornar o tipo Cat
{
return new Cat($name);
}
}
class DogShelter implements AnimalShelter
{
public function adopt(string $name): Dog // em vez de retornar o tipo Animal, pode retornar o tipo Dog
{
return new Dog($name);
}
}
$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo "\n";
$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();
O exemplo acima produzirá:
Ricky meows Mavrick barks
Contravariância
Continuando com o exemplo anterior com as classes Animal, Cat e Dog, duas classes chamadas Food e AnimalFood serão incluídas, e um método eat(AnimalFood $food) é adicionado à classe abstrata 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 o comportamento da contravariância, o método eat é substituído na classe Dog para permitir qualquer objeto do tipo Food. A classe Cat permanece inalterada.
<?php
class Dog extends Animal
{
public function eat(Food $food) {
echo $this->name . " eats " . get_class($food);
}
}
O próximo exemplo irá mostrar o comportamento da contravariância.
<?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);
O exemplo acima produzirá:
Ricky eats AnimalFood Mavrick eats Food
Mas o que acontece se $kitty tentar comer (eat()) a $banana?
$kitty->eat($banana);
O exemplo acima produzirá:
Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given