Почему php разрешает недопустимые декларации возвращаемого типа, которые, как он знает, он не может разрешить?

Насколько я могу судить, у 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;

Это приводит к

Неустранимая ошибка: Uncaught TypeError: возвращаемое значение Foo::__toString() должно иметь тип float, возвращаемая строка

Что не имеет смысла, потому что вы пытались вернуть поплавок:

Неустранимая ошибка: Uncaught Error: Метод Foo::__toString() должен возвращать строковое значение

Разве не было бы более разумно для php предотвращать объявленный тип возвращаемого значения этих типов методов, а не выдавать эти сомнительные ошибки? Если нет, то какова основная причина этого внутри? Есть ли какая-то механическая преграда, которая не позволяет php сделать это там, где он может это сделать в таких случаях, как clone?


person emptyheap    schedule 10.02.2020    source источник
comment
Вы всегда можете отправить отчет об ошибке для этой слегка неправильной обработки ошибок, которую совершенно не стоит исправлять.   -  person mario    schedule 11.02.2020
comment
Да, было бы больше смысла. На самом деле это известная проблема, и есть запрос на вытягивание открыть, чтобы исправить его (с некоторыми проблемами, которые еще не решены).   -  person rickdenhaan    schedule 11.02.2020
comment
@rickdenhaan приятно знать. Я думаю, может быть, вам следует представить это как официальный ответ. Я действительно не вижу лучшего.   -  person emptyheap    schedule 11.02.2020
comment
@emptyheap Я на самом деле более склонен закрыть это как не по теме. Это интересный вопрос, но он больше о том, как php работает внутри, а не о конкретной проблеме кодирования.   -  person rickdenhaan    schedule 12.02.2020
comment
Являются ли внутренние компоненты php оффтопом в StackOverflow? Если да, то где я должен разместить вопрос?   -  person emptyheap    schedule 12.02.2020
comment
Это не обязательно не по теме, но это также не совсем практическая проблема, на которую можно ответить, которая уникальна для разработки программного обеспечения, поскольку это не что-то не так с вашим кодом (кроме того, что просто не делайте этого), а с внутренней работой языка сам. Так что, на мой взгляд, этот конкретный вопрос лучше подходит для трекера ошибок PHP или внутреннего устройства список рассылки. Но я уверен, что есть и те, кто со мной не согласен :)   -  person rickdenhaan    schedule 13.02.2020
comment
Это либо ошибка, либо сложная для понимания функция, для которой вы не читали документацию. По этой причине этот вопрос относится к системе отслеживания ошибок PHP, где его можно найти и отследить, а не здесь. Я против использования SO в качестве дампа ошибок для проектов, у которых есть собственный трекер ошибок. Я признаю, что это не ясный случай, хотя. Если бы это было сформулировано немного иначе, это могло бы выглядеть по-другому.   -  person Ulrich Eckhardt    schedule 19.02.2020


Ответы (1)


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;
    }
}

«Почему?!», — кричите вы.

Я считаю, что есть две причины:

  1. Internals обязана высокой планке поддержки обратной совместимости.
  2. Постепенное улучшение происходит медленно, каждое улучшение основывается на предыдущих.

Поговорим о №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