PHP 7接口、返回类型提示和self

99

更新:PHP 7.4现在支持协变和逆变,解决了此问题中提出的主要问题。


我在使用PHP 7中的返回类型提示时遇到了一些问题。 我的理解是,使用: self表示您打算让实现类返回自身。 因此,我在我的接口中使用: self来表示这一点,但当我尝试实际实现接口时,我收到了兼容性错误。

以下是我遇到问题的简单演示:

interface iFoo
{
    public function bar (string $baz) : self;
}

class Foo implements iFoo
{

    public function bar (string $baz) : self
    {
        echo $baz . PHP_EOL;
        return $this;
    }
}

(new Foo ()) -> bar ("Fred") 
    -> bar ("Wilma") 
    -> bar ("Barney") 
    -> bar ("Betty");

期望的输出是:

Fred Wilma Barney Betty

实际获取的输出是:

PHP致命错误:test.php第7行,Foo::bar(int $baz)的声明必须与iFoo::bar(int $baz)兼容: Foo必须与所给接口兼容。

问题在于Foo是iFoo的实现,因此就我所知,实现应该与给定接口完全兼容。我可以通过更改接口或实现类(或两者)来使用接口名称而不是使用self返回提示使之正常,但据我了解,语义上self表示“返回刚刚调用方法的类的实例”。因此,将其更改为接口将意味着理论上我可以返回实现接口的任何实例,而我的意图是返回被调用的实例。

这是PHP的疏忽还是故意设计的?如果前者,在PHP 7.1中修复它的机会有多大?如果没有,那么正确的方式是什么?提示您的接口希望您返回刚刚调用的实例以进行链接?


我认为这是PHP返回类型提示中的错误,也许你应该将其作为错误提出;但是在这个阶段进行任何修复都不太可能进入PHP 7.1。 - Mark Baker
自从几天前发布了7.1的最新测试版以来,很不可能再有任何修复措施被加入其中。 - Charlotte Dunois
你是在哪里阅读有关self返回类型应该如何工作的解释呢? - Adam Cameron
@Adam:似乎self代表“返回调用此方法的实例,而不是实现相同接口的其他实例”是很合理的。我记得Java有类似的返回类型(虽然我已经有一段时间没有写Java程序了)。 - GordonM
1
嗨,Gordon。除非有记录表明它在某个地方可以工作,否则我不会指望可能是逻辑的事情成为现实。就我所描述的情况而言,我会尽可能地声明并使用iFoo作为返回类型。是否存在这样的情况,即它实际上无法工作?(我意识到这是“建议”/意见,而不是“答案”。 - Adam Cameron
4个回答

106

编辑说明:以下回答已过时。从PHP7.4.0开始,以下内容是完全合法的:

<?php
Interface I{
    public static function init(?string $url): self;
}
class C implements I{
    public static function init(?string $url): self{
        return new self();
    }
}
$o = C::init("foo");
var_dump($o);

原始答案:

self 指的不是实例,而是当前类。接口无法指定必须返回相同的实例 - 在您尝试使用self的方式将仅强制返回的实例为相同的类。

即便如此,在 PHP 中,返回类型声明必须是不变的,而您正在尝试的是协变的。

您对 self 的使用相当于:

interface iFoo
{
    public function bar (string $baz) : iFoo;
}

class Foo implements iFoo
{

    public function bar (string $baz) : Foo  {...}
}

这是不允许的。


返回类型声明RFC指出:

在继承期间强制声明的返回类型是不变的;这意味着当子类型覆盖父方法时,子类的返回类型必须完全匹配父类,不能省略。如果父类没有声明返回类型,则允许子类声明。

...

该RFC最初提出了协变返回类型,但由于一些问题而改为不变。将来可能会添加协变返回类型。


目前至少最好的做法是:

interface iFoo
{
    public function bar (string $baz) : iFoo;
}

class Foo implements iFoo
{

    public function bar (string $baz) : iFoo  {...}
}

20
尽管如此,我本以为将“static”作为返回类型提示应该是可行的,但实际上它甚至都没有被识别。 - Mark Baker
我想到了这种情况,这就是我要解决这个问题的方法。然而,如果可能的话,我更喜欢只使用:self,因为如果一个类实现了一个接口,那么self的返回值也隐含着接口实例的返回值。 - GordonM
1
这里有你引用的一部分的重要前言:“协变返回类型被认为是类型安全的,并在许多其他语言(如C++和Java,但我认为不包括C#)中使用。这个RFC最初提出了协变返回类型,但由于一些问题而改为不变类型。”我很好奇PHP遇到了什么问题。他们的设计选择导致了几个限制,这也导致了与traits相关的奇怪问题,在许多情况下使它们变得无用,除非你放松返回类型的限制(如下面的一些答案所示)。非常令人沮丧。 - John Pancoast
2
@MarkBaker 在 PHP 8 中,static 返回类型被添加了。 - Ricardo Boss
1
也许为了成为“最佳”选项,它也值得加上 /** @return Foo */(如果您无法升级 PHP 版本的话)。至少这样,IDE 将会建议正确的类型提示。我已在 Netbeans 中测试过。 - volvpavl
这在 PHP7.3 及之前版本是正确的,但在 PHP 7.4 中已经修复了 ^^ - hanshenrik

14

还有一种解决方案,就是在接口中不明确定义返回类型,只在PHPDoc中定义,然后您可以在实现中定义特定的返回类型:

interface iFoo
{
    public function bar (string $baz);
}

class Foo implements iFoo
{
    public function bar (string $baz) : Foo  {...}
}

2
或者使用 self 代替 Foo - instead

5

0

在我看来,这似乎是预期的行为。

只需将您的Foo :: bar方法更改为返回iFoo而不是self,然后就完成了。

解释:

接口中使用的self表示“类型为iFoo的对象”。
实现中使用的self表示“类型为Foo的对象”。

因此,接口和实现中的返回类型显然不同。

其中一条评论提到了Java以及您是否会遇到此问题。答案是肯定的,如果Java允许您编写像PHP那样的代码,那么您也会遇到相同的问题,但Java不允许您这样做。由于Java要求您使用类型名称而不是PHP的self快捷方式,因此您永远不会真正看到这一点。(有关Java中类似问题的讨论,请参见此处。)


那么声明 self,类似于声明 MyClass::class 吗? - Peter Chaula
2
@Laser 是的,是这样的。 - Moshe Katz
4
但是如果Foo实现了iFoo,那么根据定义,Foo就属于iFoo类型。 - GordonM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接