如何在 PHP 中对方法参数使用“静态”类型注释(或达到等效效果)?

Sys*_*all 3 php types covariance typeerror php-8

Foo是具有特定方法的基类。大多数这些方法使用相同的类型(例如Foo::setNext(self $foo):)。

我想创建扩展的类Foo,并且只允许使用与它们本身严格相同的类型(Extend1Foo类型的对象不能与Extend2Foo类型的对象一起使用)。

在以下代码中,由于返回类型为 static,getBar()因此引发错误。这就是我想要的。但是,setBar()由于self参数类型,允许接收 Foo 的任何实例作为参数。

可重现的例子:


class Foo 
{
    private ?self $bar = null;
    
    public function getBar(): static {
        return $this->bar;
    }
    
    public function setBar(self $object): void {
        $this->bar = $object;
    }
}

class Foo1 extends Foo { /* specific methods */ }
class Foo2 extends Foo { /* specific methods */ }

$foo1 = new Foo1;
$foo1->setBar(new Foo2); // <<< No TypeError, but I want it.
$foo2 = $foo1->getBar(); // <<< Got error, I'm OK.
Run Code Online (Sandbox Code Playgroud)

我已经强制出现 TypeError :

    public function setBar(self $object): void
    {
        if (get_class($object) != static::class) {
            throw new TypeError(
                sprintf('%s::%s(): Parameter value must be of type %s, %s given',
                    __class__, __function__,
                    static::class, get_class($object)
                )
            );
        }
        $this->child = $object;
    }
Run Code Online (Sandbox Code Playgroud)

并使用:

$foo1 = new Foo1;
$foo1->setBar(new Foo2); // TypeError : Foo::setBar(): Parameter value must be of type Foo1, Foo2 given
Run Code Online (Sandbox Code Playgroud)

这是预期的行为。

我的问题是:

有没有办法避免这种类型的动态测试?我认为static不能在参数中使用 ,而不是self,如public function setBar(static $object)

use*_*170 5

您的用例在PHP RFC 中被明确考虑并拒绝,该 RFCstatic在 returnposition 中添加了类型注释,因为允许它会破坏继承点:

static类型仅允许在返回类型中使用,它也可以作为复杂类型表达式的一部分出现,例如?staticor static|array

要理解为什么static不能用作参数类型(除了从实际角度来看这没有什么意义这一事实),请考虑以下示例:

class A {
    public function test(static $a) {}
}
class B extends A {}
 
function call_with_new_a(A $a) {
    $a->test(new A);
}

call_with_new_a(new B);
Run Code Online (Sandbox Code Playgroud)

根据里氏替换原则(LSP),我们应该能够在任何需要B类的地方替换类。A但是,在此示例中,传递B而不是A会抛出 a TypeError,因为B::test()不接受 aA作为参数。

更一般地说,static只有在协变上下文中才是合理的,目前协变上下文只是返回类型。

另一方面,可以将你想要的接口封装在一个 Trait 中,这就是 PHP 所说的 mixin:

trait Bar {
    private ?self $bar = null;
    
    public function getBar(): static {
        return $this->bar;
    }
    
    public function setBar(self $object) {
        $this->bar = $object;
    }
}

class Foo {}

final class Foo1 extends Foo { use Bar; }
final class Foo2 extends Foo { use Bar; }

try {
    $foo1 = new Foo1;
    $foo1->setBar(new Foo2); // TypeError
}
catch (Throwable $error) 
{
    echo $error->getMessage(), PHP_EOL;
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,公共方法将不会成为Foo类接口的一部分,但这可能是最好的,因为根据上面的内容,没有办法为其赋予有意义的类型签名。