共変性と反変性

PHP 7.2.0 で、子クラスのメソッドの引数の型の制限を除く形で、反変性が一部サポートされました。 PHP 7.4.0 以降で、共変性と反変性が完全にサポートされるようになりました。

共変性とは、子クラスのメソッドが、親クラスの戻り値よりも、より特定の、狭い型を返すことを許すことです。 一方で、反変性とは、親クラスのものよりも、より抽象的な、広い型を引数に指定することを許すものです。

型宣言は以下の場合に、より特定の、狭い型であると見なされます:

  • union 型 から、特定の型が削除されている場合
  • 特定の型が 交差型 に追加されている場合
  • クラスの型が、子クラスの型に変更されている場合
  • iterable が 配列 または Traversable に変更されている場合
上記と反対のことが当てはまる場合は、より抽象的な、広い型であると見なされます。

共変性

共変性がどのように動作するかを示すために、 単純な抽象クラスの親である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 " eats " get_class($food);
    }
}

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

<?php

class Dog extends Animal
{
    public function 
eat(Food $food) {
        echo 
$this->name " eats " 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 eats AnimalFood
Mavrick eats 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
関連キーワード:  クラス, 変性, メソッド, Animal, Dog, 特定, 抽象, 変更, Cat, オブジェクト