PHP - 实现接口时类型提示更具体的类型

PHP - Type Hint A More Specific Type When Implementing An Interface

给定如下 object/interface 层次结构

<?php

interface Event 
{
    public function getType(): string;
}

final class MyEvent implements Event
{
    public function getType(): string
    {
        return 'MyEvent';
    }
}

interface EventHandler
{
    public function handle(Event $event): void;
}

final class MyEventHandler implements EventHandler
{
    public function handle(MyEvent $event): void
    {
        // do something with the concrete MyEvent object...
    }
}

是否无法在从 EventHandler 接口实现 handle 方法的方法中输入更具体的 MyEvent 提示?

我的 ide 将其标记为错误

Method 'MyEventHandler::handle()' is not compatible with method 'EventHandler::handle()'.

虽然我觉得以可扩展性的名义,这应该是可能的...

实现方法的签名与接口中方法的签名不同,但在运行时不会将 MyEvent 视为与 Event 相同吗?

显然,如果具体实现类型提示 Event 接口,则没有问题,并且在运行时该方法将接受实现 MyEvent 但随后我们失去了具体 MyEventHandler 应该处理 MyEvent 类型的事件。 EventEventHandler 接口的实现可能有很多,因此能够尽可能明确是我们在这里寻找的,而不仅仅是在具体句柄中键入提示接口方法。

如果接口和类型提示无法做到这一点,是否有另一种方法可以使用抽象 class 实现这一点,同时保留强(ish)类型和传达意图?或者我们是否需要 MyEventHandler 中的显式方法来允许具体事件的类型提示 class?

你问的是“协变方法参数类型”。


问题是 MyEvent class 可以定义一些额外的行为 to/by Event 接口。由于 handler() 方法需要 MyEvent 类型(而不是 Event 类型),因此可以得出结论,handler() 方法实际上使用了该额外行为;否则为什么要改变参数的类型?

如果 PHP 允许这样做,MyEventHandler class 实例的 handle() 方法可能会收到非 MyEvent 事件对象。然后它会导致运行时错误。

事实上,维基百科对这个问题有很好的描述here。包括下面的一小段代码(代码不是 PHP,但你会得到它):

class AnimalShelter {
    void putAnimal(Animal animal) {
        //...
    }
}
class CatShelter extends AnimalShelter {

    void putAnimal(covariant Cat animal) {
        // ...
    }
}

This is not type safe. By up-casting a CatShelter to an AnimalShelter, one can try to place a dog in a cat shelter. That does not meet CatShelter parameter restrictions, and will result in a runtime error. The lack of type safety (known as the "catcall problem" in the Eiffel community, where "cat" or "CAT" is a Changed Availability or Type) has been a long-standing issue. Over the years, various combinations of global static analysis, local static analysis, and new language features have been proposed to remedy it,[7] [8] and these have been implemented in some Eiffel compilers.

有趣的是,PHP 在构造函数参数的情况下确实允许协变。

interface AnimalInterface {}


interface DogInterface extends AnimalInterface {}


class Dog implements DogInterface {}


class Pet
{
    public function __construct(AnimalInterface $animal) {}
}


class PetDog extends Pet
{
    public function __construct(DogInterface $dog)
    {
        parent::__construct($dog);
    }
}

如果您将参数从 handle() 方法移动到处理程序 classes 的构造函数,这对您的特定情况会有所帮助。