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()
выдал вам ошибку не потому, что типы были различны, а потому, что вы вообще указали тип! Это также является ошибкой компиляции:
class Foo {
public function __clone(): Foo {
return new Foo;
}
}
«Почему?!», — кричите вы.
Я считаю, что есть две причины:
- Internals обязана высокой планке поддержки обратной совместимости.
- Постепенное улучшение происходит медленно, каждое улучшение основывается на предыдущих.
Поговорим о №1. Рассмотрим этот код, который действителен вплоть до 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)
включен? Ну, потому что этот код прекрасно работает и в режиме строгих типов! Это даже имеет смысл:
<?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
, никто бы и глазом не моргнул. Единственная странная часть: имя метода "зарезервировано".
Таким образом, движок не может ни (а) обеспечить, чтобы __toString
возвращал строковый тип, ни (б) запретить вам устанавливать свой собственный тип возвращаемого значения. Потому что это нарушило бы обратную совместимость.
Что мы могли бы сделать, однако, это реализовать новый утвердительный выбор, который говорит, что эти методы не могут быть вызваны напрямую. Как только мы это сделаем, мы сможем добавить к ним вывод типа. Гипотетически:
<?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
И это пункт № 2: когда у нас есть способ защитить обратную совместимость, мы можем добавить способ принудительного применения типов к магическим методам.
Таким образом, __construct
, __destruct
, __clone
, __toString
и т. д. являются обеими (а) функциями, которые движок вызывает в определенных обстоятельствах, для которых он может разумно вывести типы, и (б) функциями, которые - исторически - могут быть вызваны напрямую способами, нарушающими разумный вывод типа из (1).
По этой причине PR 4117 исправляет Ошибка №69718 заблокирован.
Единственный способ выйти из этого тупика: разработчик соглашается на обещание, что эти методы нельзя вызывать напрямую. Это освобождает движок для применения строгих правил вывода типов.
person
bishop
schedule
19.02.2020