我想创建一个具有抽象方法的抽象类,允许在返回类型中使用抽象类型。在我的最终类中,我想用实现最初声明的抽象类型的类型覆盖返回类型。
<?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
在文档中,共变是用接口来解释的。 但不是抽象类。更多关于PHP的实现,文档中说。
在PHP 7.2.0中,通过取消子方法中参数的类型限制,引入了部分共变性。从 PHP 7.4.0 开始,增加了对完全协方差和协方差的支持。
我使用的是 PHP 7.4。
面向对象程序设计的一个相当核心的原则是 "抽象"。利斯科夫替代原理 本质上归结为。
如果
S
是T,
那么T
的对象来代替。S
而不改变该程序的任何理想特性。
实现这一目标的方法是拥有 共变 方法返回类型。违变 方法类型的参数。抛出的异常在这里算是返回类型,所以它们也需要是共变的。
你需要的是 协方差 的类型参数,打破了这一原则。原因可以从下面的例子中看出。
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
和 C
也是一个孩子 A
通过简单的向下转述(这是允许的),这个限制被绕过了。你总是可以禁止向下转换,但这几乎是说你在禁止继承,而继承是OOP的一个核心原则。
现在当然有令人信服的理由来允许类型参数的共变,这就是为什么一些语言(如 Eiffel)允许的,但这是一个公认的问题,甚至被命名为 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;
}
}
然而这有点混乱,而且可能没有必要。