共変性と反変性

PHP 7.4.0 より前のバージョンでは、クラスのメソッドは 不変 な引数の型と、不変 な戻り値の型をサポートしていました。 これは、T という型を引数と戻り値に持つメソッドが親クラスにあった場合、 対応する子クラスのメソッドの引数や戻り値の型 もまた T でなければならない ということです。

PHP 7.4.0 以降では、共変性 と 反変性 の概念がサポートされています。 一方でこれらはややこしく 聞こえる かもしれませんが、実際はかなり単純、かつオブジェクト指向プログラミングに非常に役立つものです。

共変性

共変性 とは、子クラスのメソッドが、親クラスの返り値よりも、より特定の、狭い型を返すことを許すことです。 これは、例で示したほうが良いでしょう。

単純な抽象クラスの親であるAnimal から始めましょう。 このクラスは子クラス CatDog に継承されています。

<?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";
    }
}

この例では、どのメソッドも値を返さないことに注意して下さい。 以下ではこれらのクラスを使い、 Animal, Cat または Dog クラスの新しいオブジェクトを返すファクトリをいくつか作ってみることにします。 以下では、共変性の実際の例が出てきます。

<?php

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

class 
CatShelter implements AnimalShelter
{
    public function 
adopt(string $name): Cat // Animal 型を返す代わりに、Cat型を返すことができる
    
{
        return new 
Cat($name);
    }
}

class 
DogShelter implements AnimalShelter
{
    public function 
adopt(string $name): Dog // Animal 型を返す代わりに、Dog型を返すことができる
    
{
        return new 
Dog($name);
    }
}

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

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

上の例の出力は以下となります。

Ricky meows
Mavrick barks

反変性

一方で 反変性 は、子クラスのメソッドで、 親クラスのものよりも、より抽象的な、広い型を引数に指定することを許すものです。 既に示した Animal, Cat および Dog クラスの例を引き続き使い、 FoodAnimalFood クラスを追加し、 Animal 抽象クラスに eat(AnimalFood $food) メソッドを追加してみましょう。

<?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 " nom noms " get_class($food);
    }
}

反変性 の振る舞いを見るため、Dog クラスの eat メソッドをオーバーライドし、あらゆる Food 型のオブジェクトを受け入れることにします。 Cat クラスは変更していません。

<?php

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

さて、反変性がどのように動くかが以下でわかるでしょう。

<?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);

上の例の出力は以下となります。

Ricky nom noms AnimalFood
Mavrick nom noms Food

しかし、$kittyeat メソッドに $banana を渡すとどうなるでしょう?

$kitty->eat($banana);

上の例の出力は以下となります。

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

User Contributed Notes 2 notes

up
6
xedin dot unknown at gmail dot com
1 month 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
1
Anonymous
15 days 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