Суперкласс PHP вызывает методы подкласса, не зная о них?

Пытаясь отлаживать некоторые классы PHP, я столкнулся с некоторым поведением, которое, на мой взгляд, действительно странно.

Я построил демонстрацию поведения ниже:

class BaseClass {
   public function baseMethod () {
      echo (implode (' ', $this -> childMethod ()) . PHP_EOL);
   }
}

class ChildClass extends BaseClass {
   protected function childMethod () {
      return array ('What', 'The', 'Actual', 'Fork!');
   }
}

$a = new ChildClass ();
$a -> baseMethod ();

Теперь, на мой взгляд, базовый класс вообще не должен иметь возможности делать какие-либо предположения о подклассе, кроме тех, которые он применяет для подкласса, объявляя (или наследуя) абстрактные методы или реализуя интерфейс. Однако приведенный выше код фактически выводит строку и не выдает никаких ошибок!

Какая настоящая вилка!

Это кажется мне нарушенным поведением. Если базовый класс не объявит abstract protected function childMethod();, он не сможет его вызвать, не так ли?

Я искал в Интернете, чтобы попытаться найти что-то, что демонстрирует ожидаемое поведение. Пока все, что мне удалось найти, это следующее из руководства по PHP:

Видимость с других объектов

Объекты одного типа будут иметь доступ к закрытым и защищенным членам друг друга, даже если они не являются одними и теми же экземплярами. Это связано с тем, что конкретные детали реализации уже известны, когда они находятся внутри этих объектов.

Так правильно ли поведение, которое я здесь наблюдаю, или это ошибка в PHP? Это определенно не то поведение, на которое я бы полагался, потому что оно кажется мне неправильным.

К вашему сведению, проблема, которую мы обнаружили в реальном коде, заключалась в том, что подкласс объявил закрытый метод, который суперкласс пытался вызвать. Суперкласс не объявлял абстрактный метод (а если бы и сделал, то должен был бы быть как минимум защищен).


person GordonM    schedule 29.01.2013    source источник
comment
Я не думаю, что это ошибка, хотя $this является ссылкой на вызывающий объект (ChildClass в вашем примере), у которого есть метод childMethod, поэтому скрипт работает. Я бы сказал, что это технически возможно, но это плохая практика.   -  person Leri    schedule 29.01.2013
comment
@PLB Но что, если подкласс не реализует childMethod ()? В BaseClass нет ничего, что заставляло бы ChildClass его реализовать.   -  person GordonM    schedule 29.01.2013
comment
Вероятно, вызывает Вызов неопределенного метода   -  person Leri    schedule 29.01.2013
comment
Это было бы ошибкой времени компиляции в языке со статической типизацией, таком как C#. Но это PHP...   -  person Palantir    schedule 29.01.2013
comment
@PLB Это то, что я ожидал, но, как я уже сказал в своем вопросе, это не то, что на самом деле происходит.   -  person GordonM    schedule 29.01.2013
comment
@GordonM Вызовы функций (и доступ к переменным в этом отношении) оцениваются во время выполнения, а не во время компиляции или при создании объекта.   -  person dualed    schedule 29.01.2013
comment
Попытка найти связанные сообщения об ошибках - вот этот bugs.php.net/bug.php?id =40840, что похоже и предполагает, что здесь действительно должна возникать ошибка во время выполнения.   -  person Paul Dixon    schedule 29.01.2013
comment
@GordonM Я согласен, что это не ожидаемое поведение, но это не может быть ошибкой, поскольку $this относится к объекту, а не к самому классу. Там self относится к текущему классу.   -  person Leri    schedule 29.01.2013


Ответы (3)


Теперь, на мой взгляд, базовый класс вообще не должен иметь возможности делать какие-либо предположения о подклассе, кроме тех, которые он применяет для подкласса, объявляя (или наследуя) абстрактные методы или реализуя интерфейс.

Вот как все работает в статически типизированных языках. В PHP (и многих других) вы можете вызывать любой метод для любого значения (это даже не обязательно должен быть объект). Если вызов не имеет смысла, вы получите ошибку времени выполнения.

Конечно, неплохо объявить метод, если вы ожидаете, что производные классы будут его реализовывать; это просто хорошая практика. Кроме того, многие известные PHP IDE обнаружат такое упущение и пометят вызов, чтобы привлечь к нему ваше внимание именно потому, что компилятор не может этого сделать.

Это кажется мне нарушенным поведением. Если базовый класс не объявит abstract protected function childMethod();, он не сможет его вызвать, не так ли?

Так и должно быть, как описано выше. Если вы находите такое поведение нежелательным, PHP — это не тот язык, который вам следует использовать.

Кроме того: в тесно связанной заметке вы также можете найти нелогичным, что базовый класс может без проблем обращаться к protected членам, определенным в производном классе. Это задокументировано. На самом деле это именно то, что происходит в вашем примере, но я упоминаю об этом специально, потому что это другой вид контринтуитивности (ваш пример также был бы загадочным, если бы метод был public).

Рассмотрим бесконечный список конструкций, которые допускает PHP, которые также «не должны быть возможными» с точки зрения статически типизированной, включая:

// #1
$varName = 'foo';

// How do you know $object has a property named "foo"?
// How do you know that "foo" is a valid property name in the first place?
// How do you know that $object is an object to begin with?
echo $object->$varName;

// #2
$object = new SomeObject;
$methodName = 'someMethod';

// it's practically impossible to reason about this before runtime
call_user_func(array($object, $methodName));
person Jon    schedule 29.01.2013

Это не ошибка. Это должно работать так. Попробую прокомментировать вызов метода:

class BaseClass {
   public function baseMethod () {
      echo (implode (' ', $this -> childMethod ()) . PHP_EOL);
   }
}

class ChildClass extends BaseClass {
   protected function childMethod () {
      return array ('What', 'The', 'Actual', 'Fork!');
   }
}

/****/

$a = new ChildClass ();
/** $a is now instance of ChildClass, that is a sublass of BaseClass
 *  so the Object $a has 2 methods: baseMethod() inherited from BaseClass and childMethod() from ChildClass.
 */

$a -> baseMethod ();
/** now you're calling $a->baseMethod(), which will do a method call on $this->childMethod(). 
 *  As $this is refering $a here, $a has indeed a method named childMethod(), that will be called.
 */

Что вас интересует, так это видимость с protected. Но поскольку $a является типом ChildClass, защищенный метод обязательно виден ему и унаследованному baseMethod(). Это должно быть одинаково на каждом языке ООП.

Плохая часть вашего кода — это предположение внутри baseMethod() о вызове childMethod(). Это всегда будет завершаться ошибкой, если такого метода нет. Языки ООП, такие как Java, вызовут здесь ошибку компиляции, но PHP этого не сделает, поскольку в PHP нет предварительного компилятора. Если вы измените инициализацию, например. $a = new ChildClass (); до $a = new BaseClass ();, вы также получите ошибку времени выполнения в PHP.

person ConcurrentHashMap    schedule 29.01.2013
comment
Проблема с этим, по крайней мере для меня, заключается в том, что это означает, что вы можете написать базовый класс и предположить, что все его подклассы реализуют некоторую функциональность, фактически явно не заявляя об этом факте. Предположим, у меня есть OtherChildClass extends BaseClass, но я не реализовал в нем childMethod() или реализовал частный childMethod(). Ничто не мешает мне это сделать, потому что я не поместил абстрактный childMethod() в базовый класс. - person GordonM; 29.01.2013
comment
Да все верно. В PHP нет предварительной проверки, поэтому у вас всегда будут ошибки во время выполнения в PHP, если вы написали код неправильно. Могут быть еще некоторые IDE для PHP, которые заметят такую ​​​​ошибку. - person ConcurrentHashMap; 29.01.2013

PHP очень динамичный язык и позволяет использовать полиморфизм без наследования. В этом случае он просто ищет метод «childMethod» во время выполнения и, поскольку он существует, вызывает его. Как вы получили этот метод, не важно. Вы также можете сгенерировать этот метод динамически с помощью магического метода __call. Вы никогда не сможете сделать это в C++ или Java, но в таких языках, как PHP или Javascript, все в порядке, и это дает вам гораздо больше возможностей.

person Josef Kufner    schedule 29.01.2013
comment
Хотя вы в основном правы, PHP на самом деле является уродливым потомком Лебедя и Кота, оба могут быть красивыми по-своему, но комбинация - просто уродливая вещь с клювами и когтями, которые вонзаются вам в спину. - person dualed; 29.01.2013