## 共変性と反変性

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

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

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

- union 型 から、特定の型が削除されている場合
- 特定の型が 交差型 に追加されている場合
- クラスの型が、子クラスの型に変更されている場合
- iterable が 配列 または Traversable に変更されている場合

上記と反対のことが当てはまる場合は、より抽象的な、広い型であると見なされます。

### 共変性

共変性がどのように動作するかを示すために、 単純な抽象クラスの親であるAnimal を作ることにします。 このクラスは子クラス Cat と 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";
        }
    }

この例では、どのメソッドも値を返さないことに注意して下さい。 以下ではこれらのクラスを使い、 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 クラスの例を引き続き使い、 Food と AnimalFood クラスを追加し、 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

しかし、$kitty の eat() メソッドに $banana を渡すとどうなるでしょう？

    $kitty->eat($banana);

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

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

### プロパティの共変性と反変性(変性)

デフォルトでは、プロパティは共変でも反変でもなく不変です。 つまり、子クラスでは型は変更できません。 「get」操作は共変でなければならず、 「set」操作は反変でなければならないことが理由です。 双方を同時に満たすには、プロパティは不変である必要があります。

PHP 8.4.0 から、インターフェイスや抽象クラスでの抽象プロパティや、 仮想プロパティ が追加されたことにより、 プロパティが「get」または「set」だけを持つことを宣言できるようになりました。 つまり、「get」操作だけが必要な抽象プロパティや仮想プロパティは共変性を持ちます。 同様に、「set」操作だけが必要な抽象プロパティや仮想プロパティは反変性を持ちます。

ただし、いったんプロパティが「get」と「set」操作の両方を持つようになると、 それ以上の拡張において共変あるいは反変にはなりません。 その時点で不変となります。

例1 プロパティの型の変性

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

    interface PetOwner
    {
        // 必要なのは「get」操作のみなので、共変です
        public Animal $pet { get; }
    }

    class DogOwner implements PetOwner
    {
        // 「get」が Animal を返す限り、より狭い型に変更できます。
        // しかし、これは通常のプロパティなので、
        // 子クラスでは型を変更できません。
        public Dog $pet;
    }

    class PoodleOwner extends DogOwner
    {
        // これは許可されません。
        // DogOwner::$pet は「get」「set」両方の操作を持つためです。
        public Poodle $pet;
    }
    ?>

`↑` and `↓` to navigate • `Enter` to select • `Esc` to close • `/` to open