为什么php允许它知道不允许的无效返回类型修饰?

时间:2020-02-10 22:50:27

标签: php php-internals

据我所知,php有能力防止在知道有问题的地方声明返回类型。

class Foo {
    public function __clone(): Baz {
        return new Baz;
    }
}

class Baz {

}

$foo = new Foo;
$newFoo = clone $foo;

这会产生Fatal error: Clone method Foo::__clone() cannot declare a return type,这是非常明智的。

但是为什么PHP会允许这样的事情:

class Foo {
    public function __toString(): float {
        return "WAT!?!";
    }
}

echo new Foo;

这导致

致命错误:未捕获的TypeError:Foo :: __ toString()的返回值必须为float类型,返回的字符串为

这没有任何意义,因为您是否要尝试返回浮点数:

致命错误:未捕获错误:方法Foo :: __ toString()必须返回字符串值

对于PHP来说,阻止这些方法的声明的返回类型而不是给出那些可疑的错误会更有意义吗?如果不是,内部原因是什么?是否有一些机械障碍阻止php在 clone 这样的情况下可以做到这一点?

1 个答案:

答案 0 :(得分:4)

TL; DR:在魔术方法上支持类型推断会破坏向后兼容性。

示例:此代码输出什么?

$foo = new Foo;
$bar = $foo->__construct();
echo get_class($bar);

如果您说Foo,则不正确:是Bar


PHP在返回类型处理方面经过了漫长而复杂的发展。

  • 在PHP 7.0之前,返回类型提示是解析错误。
  • 在PHP 7.0中,我们使用非常简单的规则(RFC)获得了返回类型声明,在经历了有争议的内部辩论之后,我们获得了严格的类型(RFC)。
  • PHP在协方差和对数方差中出现了一些奇怪的现象,直到PHP 7.4为止,在那里我们得到了许多排序(RFC)。

今天的行为反映了这种有机增长,疣和一切。

您指出__clone()行为是明智的,然后将其与看似无意义的__toString()行为进行比较。我质疑在任何类型推断的合理预期下,它们中的都不是

这是__clone引擎代码:

6642     if (ce->clone) {                                                          
6643         if (ce->clone->common.fn_flags & ZEND_ACC_STATIC) {                   
6644             zend_error_noreturn(E_COMPILE_ERROR, "Clone method %s::%s() cannot be static",
6645                 ZSTR_VAL(ce->name), ZSTR_VAL(ce->clone->common.function_name));
6646         } else if (ce->clone->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {   
6647             zend_error_noreturn(E_COMPILE_ERROR,                              
6648                 "Clone method %s::%s() cannot declare a return type",         
6649                 ZSTR_VAL(ce->name), ZSTR_VAL(ce->clone->common.function_name));
6650         }                                                                     
6651     }                                                                         

请特别注意该措辞(强调我的意思):

克隆方法 ... 无法声明返回类型

__clone()给您一个错误,不是因为类型是不同,而是因为您完全给出了一个类型!这也是compile error

class Foo {
    public function __clone(): Foo {
        return new Foo;
    }
}

“为什么?!”,你尖叫。

我相信有两个原因:

  1. 内部依赖于向后兼容性维护的高标准。
  2. 渐进式改进进展缓慢,每次改进都基于较早的改进。

让我们谈谈#1。考虑this code,它一直到PHP 4.0都有效:

<?php
class Amount {
    var $amount;
}
class TaxedAmount extends Amount {
    var $rate;
    function __toString() {
        return $this->amount * $this->rate;
    }
}
$item = new TaxedAmount;
$item->amount = 242.0;
$item->rate = 1.13;
echo "You owe me $" . $item->__toString() . " for this answer.";

一些可怜的灵魂以一种完全合理的方式使用__toString作为自己的方法。现在,保留其行为是当务之急,因此我们无法对破坏该代码的引擎进行更改。这就是进行strict_types声明的动机:允许对解析器行为进行选择加入更改,以便在继续添加新行为的同时保持旧行为不变。

您可能会问:declare(strict_types=1)启用后,为什么我们不解决这个问题?好吧,因为this code在严格类型模式下也完全有效!它甚至使sense

<?php declare(strict_types=1);

class Amount {
    var $amount;
}
class TaxedAmount extends Amount {
    var $rate;
    function __toString(): float {
        return $this->amount * $this->rate;
    }
}
$item = new TaxedAmount;
$item->amount = 242.0;
$item->rate = 1.13;
echo "You owe me $" . $item->__toString() . " for this answer.";

此代码无异味。这是有效的PHP代码。如果将该方法称为getTotalAmount而不是__toString,那么谁也不会惊讶。唯一奇怪的部分:方法名是“ reserved”。

因此,引擎既不能(a)强制执行__toString返回字符串类型,也不能(b)阻止您设置自己的返回类型。因为这样做都会违反向后兼容性。

但是,我们可以做的是实现一个新的肯定选择加入,该选择不能直接调用这些方法。一旦这样做,就可以向它们添加类型推断。假设地:

<?php declare(strict_magic=1);

class Person {
    function __construct(): Person {
    }
    function __toString(): string {
    }
    // ... other magic
}

(new Person)->__construct(); // Fatal Error: cannot call magic method on strict_magic object

这是第二点:一旦我们有了一种保护向后兼容性的方法,就可以添加一种在魔术方法上强制类型的方法。

总而言之,__construct__destruct__clone__toString等都是(a)在某些情况下引擎可以合理推断的函数类型和(b)函数-过去-可以以违反(1)中的合理类型推断的方式直接调用。

这是PR 4117修复Bug #69718的原因。

打破这种僵局的唯一方法:开发人员选择承诺不能直接调用这些方法。这样做释放了引擎执行严格的类型推断规则的能力。