Kovarianz und Kontravarianz

Mit PHP 7.2.0 wurde teilweise Kontravarianz eingeführt, indem Typeneinschränkungen bei Parametern von Kindmethoden entfernt wurden. Mit PHP 7.4.0 wurde dann vollständige Unterstützung für Kovarianz und Kontravarianz eingeführt.

Kovarianz erlaubt es den Methoden eines Kindes, einen spezifischeren Typen als die Elternmethode für den Rückgabewert zurückzugeben. Auf der anderen Seite erlaubt die Kontravarianz einen weniger spezifischen Parametertypen in einer Kindmethode als in der Elternmethode.

Eine Typdeklaration wird in den folgenden Fällen als spezifischer angesehen:

  • Ein Typ wird aus einem Vereinigungstypen entfernt
  • Ein Klassentyp wird zu dem Typen eines seiner Kinder geändert
  • Der Typ float wird zu int geändert
Falls das Gegenteil zutrifft, wird ein Klassentyp als weniger spezifisch angesehen.

Kovarianz

Um Kovarianz zu illustrieren, wird eine einfache abstrakte Elternklasse Tier erzeugt. Tier wird von seinen Kindern Katze und Hund beerbt.

<?php

abstract class Tier
{
    protected 
string $name;

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

    abstract public function 
gibLaut();
}

class 
Hund extends Tier
{
    public function 
gibLaut()
    {
        echo 
$this->name " bellt";
    }
}

class 
Katze extends Tier 
{
    public function 
gibLaut()
    {
        echo 
$this->name " miaut";
    }
}

Beachtenswert ist, dass keine der Methoden hier einen Wert zurückgibt. Es werden nun ein paar Factories hinzugefügt, die ein neues Objekt der Klassen Tier, Katze oder Hund erzeugen.

<?php

interface TierHeim
{
    public function 
adoptiere(string $name): Tier;
}

class 
KatzenHeim implements TierHeim
{
    public function 
adoptiere(string $name): Katze // statt den Klassentyp Tier zurückzugeben, kann hier Typ Katze verwendet werden
    
{
        return new 
Katze($name);
    }
}

class 
HundeHeim implements TierHeim
{
    public function 
adoptiere(string $name): Hund // statt den Klassentyp Tier zurückzugeben, kann hier Typ Hund verwendet werden
    
{
        return new 
Hund($name);
    }
}

$kaetzchen = (new KatzenHeim)->adoptiere("Ricky");
$kaetzchen->gibLaut();
echo 
"\n";

$huendchen = (new HundeHeim)->adoptiere("Mavrick");
$huendchen->gibLaut();

Das oben gezeigte Beispiel erzeugt folgende Ausgabe:

Ricky miaut
Mavrick bellt

Kontravarianz

Um das vorherige Beispiel mit den Klassen Tier, Katze und Hund fortzusetzen, werden nun die Klassen Nahrung sowie Tierfutter definiert, sowie auch eine Methode iss(Tierfutter $futter) zur abstrakten Klasse Tier hinzugefügt.

<?php

class Nahrung {}

class 
Tierfutter extends Nahrung {}

abstract class 
Tier
{
    protected 
string $name;

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

    public function 
iss(Tierfutter $futter)
    {
        echo 
$this->name " isst " get_class($futter);
    }
}

Um das Verhalten der Kontravarianz zu sehen, wird nun die Methode iss in der Klasse Hund überschrieben, um jedes Objekt der Klasse Nahrung zuzulassen. Die Klasse Katze bleibt unverändert.

<?php

class Hund extends Tier
{
    public function 
iss(Nahrung $futter) {
        echo 
$this->name " isst " get_class($futter);
    }
}

Das folgende Beispiel zeigt das Verhalten der Kontravarianz.

<?php

$kaetzchen 
= (new KatzenHeim)->adoptiere("Ricky");
$katzenFutter = new Tierfutter();
$kaetzchen->iss($katzenFutter);
echo 
"\n";

$huendchen = (new HundeHeim)->adoptiere("Mavrick");
$banane = new Nahrung();
$huendchen->iss($banane);

Das oben gezeigte Beispiel erzeugt folgende Ausgabe:

Ricky isst Tierfutter
Mavrick isst Nahrung

Was geschieht nun aber, wenn man der iss-Methode von $kaetzchen versucht die $banane zu übergeben?

$kitty->iss($banane);

Das oben gezeigte Beispiel erzeugt folgende Ausgabe:

Fatal error: Uncaught TypeError: Argument 1 passed to Tier::iss() must be an instance of Tierfutter, instance of Nahrung given
add a note add a note

User Contributed Notes 4 notes

up
37
xedin dot unknown at gmail dot com
4 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
2
Anonymous
4 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>';
up
0
maxim dot kainov at gmail dot com
3 years ago
This example will not work:

<?php

class CatFood extends AnimalFood { }

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

?>

The reason is:

<?php
   
class DogFood extends AnimalFood { }
  
    function
feedAnimal(Animal $animal, AnimalFood $food) {
       
$animal->eat($food);  
    }

   
$cat = new Cat();
   
$dogFood = new DogFood();  

   
feedAnimal($cat, $dogFood);   
?>

But you can do it with traits, like this:

<?php

trait AnimalTrait
{
    public function
eat(AnimalFood $food)
    {
        echo
$this->name . " ест " . get_class($food);
    }
}

class
Cat
{
    use
AnimalTrait;

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

?>
up
-2
phpnet-at-kennel17-dotco-dotuk
3 years ago
Following the examples above, you might assume the following would be possible.

<?php

class CatFood extends AnimalFood { ... }

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

?>

However, the Liskov Substitution Prinicpal, and therefore PHP, forbids this.  There's no way for cats to eat cat food, if animals are defined as eating animal food.

There are a large number of legitimate abstractions that are forbidden by PHP, due to this restriction.
To Top