共変性と反変性
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
User Contributed Notes 3 notes
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`.
The gist of how the Liskov Substition Princple applies to class types is, basically: "If an object is an instance of something, it should be possible to use it wherever an instance of something is allowed". The Co- and Contravariance rules come from this expectation when you remember that "something" could be a parent class of the object.
For the Cat/Animal example of the text, Cats are Animals, so it should be possible for Cats to go anywhere Animals can go. The variance rules formalise this.
Covariance: A subclass can override a method in the parent class with one that has a narrower return type. (Return values can be more specific in more specific subclasses; they "vary in the same direction", hence "covariant").
If an object has a method you expect to produce Animals, you should be able to replace it with an object where that method produces only Cats. You'll only get Cats from it but Cats are Animals, which are what you expected from the object.
Contravariance: A subclass can override a method in the parent class with one that has a parameter with a wider type. (Parameters can be less specific in more specific subclasses; they "vary in the opposite direction", hence "contravariant").
If an object has a method you expect to take Cats, you should be able to replace it with an object where that method takes any sort of Animal. You'll only be giving it Cats but Cats are Animals, which are what the object expected from you.
So, if your code is working with an object of a certain class, and it's given an instance of a subclass to work with, it shouldn't cause any trouble:
It might accept any sort of Animal where you're only giving it Cats, or it might only return Cats when you're happy to receive any sort of Animal, but LSP says "so what? Cats are Animals so you should both be satisfied."
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>';