PHP 与继承 class 的协方差 - 声明不兼容

PHP Covariance with inherited class - Declarations incompatibles

我想用抽象方法创建一个抽象 class,它允许 return 类型中的抽象类型。在我的最终 class 中,我想用实现最初声明的抽象类型的类型覆盖类型 returned。

<?php

abstract class A {
    abstract public function test(A $foo): self;
}

class B extends A {
    public function test(B $foo): self
    {
        return $this;
    }
}

抛出这个编译错误:

Fatal error: Declaration of B::test(B $foo): B must be compatible with A::test(A $foo): A in ... on line 8

In documentation, covariance is explained with interface. 但不是摘要 class。有关 PHP 实施的更多信息,文档说:

In PHP 7.2.0, partial contravariance was introduced by removing type restrictions on parameters in a child method. As of PHP 7.4.0, full covariance and contravariance support was added.

我正在使用 PHP 7.4.

面向 object 的编程的一个相当核心的原则是 Liskov substitution principle,它本质上可以归结为:

if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program

实现此目的的方法是使用协变方法return类型,逆变方法类型参数。抛出的异常在这里算作 return 类型,因此它们也需要是协变的。

你需要的是 covariance 类型参数,它打破了这个原则。 原因可以从下面的例子中看出:

abstract class A {
    abstract public function test(A $foo): self;
}

class C extends A {
    public function test(C $foo): self {
        return $this;
    }
}

class B extends A {
    public function test(B $foo): self {
        return $this;
    }
}

$b = new B();
$c = new C();

$b->test($c); // Does not work
((A)$b)->test((A)$c); // Works


在上面的示例中,您不允许 B::test 接受 B 以外的任何类型作为类型参数。然而,由于 B 本身是 A 的 child 并且 C 也是 A 的 child 通过简单的 down-casting (这被允许)限制被绕过。你总是可以禁用 down-casting 但这几乎是说你正在禁用继承,这是 OOP 的核心原则。

现在当然有令人信服的理由允许类型参数的协变,这就是为什么某些语言(例如 Eiffel) allow it, however this is recognised to be a problem and even has been given the name CATcalling(CAT 代表更改的可用性或类型)。

在 PHP 中,您可以尝试进行运行时检查以纠正这种情况:

abstract class A {
    abstract public function test(A $foo) {
         // static keyword resolve to the current object type at runtime 
         if (!$foo instanceof static) { throw new Exception(); }  
    }
}

class C extends A {
    public function test(A $foo): self {
        parent::test($foo);
        return $this;
    }
}

class B extends A {
    public function test(A $foo): self {
        parent::test($foo);
        return $this;
    }
}

然而,这有点混乱而且可能没有必要。