Развертывание области в конструкторах PHP-класса

Я изучаю PHP-классы и исключения, и, исходя из фона C ++, следующее выглядит как нечетное:

Когда конструктор производного класса выдает исключение, кажется, что деструктор базового класса не запускается автоматически:

class Base { public function __construct() { print("Base const.\n"); } public function __destruct() { print("Base destr.\n"); } } class Der extends Base { public function __construct() { parent::__construct(); $this->foo = new Foo; print("Der const.\n"); throw new Exception("foo"); // #1 } public function __destruct() { print("Der destr.\n"); parent::__destruct(); } public $foo; // #2 } class Foo { public function __construct() { print("Foo const.\n"); } public function __destruct() { print("Foo destr.\n"); } } try { $x = new Der; } catch (Exception $e) { } 

Это печатает:

 Base const. Foo const. Der const. Foo destr. 

С другой стороны, деструкторы объектов-членов выполняются должным образом, если в конструкторе есть исключение (в #1 ). Теперь я задаюсь вопросом: как реализовать правильную развертку областей в иерархии классов в PHP, чтобы подобъекты были правильно уничтожены в случае исключения?

Кроме того, кажется, что невозможно запустить базовый деструктор после уничтожения всех объектов-членов (на #2 ). Для этого, если мы удалим строку #1 , получим:

 Base const. Foo const. Der const. Der destr. Base destr. Foo destr. // ouch!! 

Как решить эту проблему?

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

Я хотел бы объяснить, почему PHP ведет себя таким образом и почему это фактически делает (некоторым) смысл.

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

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

  1. PHP запускает shutdown, поэтому все ссылки на переменные удаляются.
  2. Когда ссылка, созданная с помощью $x (экземпляру Der ), удаляется, объект уничтожается.
  3. Вызывается производный деструктор, который вызывает базовый деструктор.
  4. Теперь ссылка из $this->foo на экземпляр Foo удаляется (как часть уничтожения полей члена).
  5. Больше нет ссылок на Foo , поэтому он также уничтожается, и деструктор вызывается.

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

В случае Exception вам нужно понять, что для PHP никогда не существовал экземпляр класса, поскольку конструктор так и не вернулся. Как вы можете разрушить то, что никогда не строилось?


Как это исправить?

Вы этого не сделаете. Сам факт, что вам нужен деструктор, вероятно, является признаком плохого дизайна. И тот факт, что порядок уничтожения имеет большое значение для вас, – это еще больше.

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

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

 class Base { public $x; // ... (constructor, destructor) } class Derived extends Base { public $foo; // ... (constructor, destructor) } 

Когда я создаю экземпляр, $z = new Derived; , то сначала создается подчиненный объект Base , затем объекты-члены Derived (а именно $z->foo ) и, наконец, конструктор Derived выполняется.

Поэтому я ожидал, что последовательность уничтожения произойдет в совершенно противоположном порядке:

  1. выполнить Derived деструктор

  2. уничтожить объекты-члены Derived

  3. выполнить Base деструктор.

Однако, поскольку PHP не ссылается на базовые деструкторы или базовые конструкторы неявно, это не сработает, и мы должны сделать основной вызов деструктора явным внутри производного деструктора. Но это нарушает последовательность уничтожения, которая теперь является «производной», «базовой», «членами».

Вот моя забота: если какой-либо из объектов-членов требует, чтобы состояние базового подобъекта было действительным для их собственной операции, то ни один из этих объектов-членов не может полагаться на этот подобъект базы данных во время их собственного уничтожения, поскольку этот базовый объект уже был признан недействительным ,

Это настоящая забота, или есть что-то на языке, который предотвращает подобные зависимости?

Вот пример в C ++, который демонстрирует необходимость правильной последовательности уничтожения:

 class ResourceController { Foo & resource; public: ResourceController(Foo & rc) : resource(rc) { } ~ResourceController() { resource.do_important_cleanup(); } }; class Base { protected: Foo important_resource; public: Base() { important_resource.initialize(); } // constructor ~Base() { important_resource.free(); } // destructor } class Derived { ResourceController rc; public: Derived() : Base(), rc(important_resource) { } ~Derived() { } }; 

Когда я создаю Derived x; , тогда базовый подобъект строится первым, который устанавливает important_resource . Затем объект-член rc инициализируется ссылкой на important_resource , который требуется при уничтожении rc . Поэтому, когда время жизни x заканчивается, производный деструктор вызывается первым (ничего не делая), тогда rc уничтожается, выполняя его работу по очистке, и только тогда Base подобъект уничтожается, освобождая important_resource .

Если разрушение произошло не по порядку, тогда деструктор rc имел бы доступ к недопустимой ссылке.

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

Теперь я задаюсь вопросом: как реализовать правильную развертку областей в иерархии классов в PHP, чтобы подобъекты были правильно уничтожены в случае исключения?

В примере, который вы даете, нет ничего, чтобы расслабиться. Но для игры предположим, вы знаете, что базовый конструктор может выкинуть exeception, но вам нужно инициализировать $this->foo прежде чем позвонить ему.

Затем вам нужно только поднять значение « $this » на один (временно), для этого требуется (немного) больше, чем локальная переменная в __construct , давайте выложим это на $foo :

 class Der extends Base { public function __construct() { parent::__construct(); $this->foo = new Foo; $this->foo->__ref = $this; # <-- make base and Der __destructors active print("Der const.\n"); throw new Exception("foo"); // #1 unset($this->foo->__ref); # cleanup for prosperity } с class Der extends Base { public function __construct() { parent::__construct(); $this->foo = new Foo; $this->foo->__ref = $this; # <-- make base and Der __destructors active print("Der const.\n"); throw new Exception("foo"); // #1 unset($this->foo->__ref); # cleanup for prosperity } 

Результат:

 Base const. Foo const. Der const. Der destr. Base destr. Foo destr. 

демонстрация

Подумайте сами, если вам нужна эта функция или нет.

Чтобы управлять порядком, когда вызывается деструктор Foo, отключите свойство в деструкторе, как показано в этом примере .

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

 Der const. Base const. Foo const. Foo destr. Base destr. Der destr. 

осуществляется с помощью:

 class Base { public function __construct() { print("Base const.\n"); } public function __destruct() { print("Base destr.\n"); } } class Der extends Base { public function __construct() { print("Der const.\n"); parent::__construct(); $this->foo = new Foo; $this->foo->__ref = $this; # <-- make Base and Def __destructors active throw new Exception("foo"); unset($this->foo->__ref); } public function __destruct() { unset($this->foo); parent::__destruct(); print("Der destr.\n"); } public $foo; } class Foo { public function __construct() { print("Foo const.\n"); } public function __destruct() { print("Foo destr.\n"); } } try { $x = new Der; } catch (Exception $e) { } с class Base { public function __construct() { print("Base const.\n"); } public function __destruct() { print("Base destr.\n"); } } class Der extends Base { public function __construct() { print("Der const.\n"); parent::__construct(); $this->foo = new Foo; $this->foo->__ref = $this; # <-- make Base and Def __destructors active throw new Exception("foo"); unset($this->foo->__ref); } public function __destruct() { unset($this->foo); parent::__destruct(); print("Der destr.\n"); } public $foo; } class Foo { public function __construct() { print("Foo const.\n"); } public function __destruct() { print("Foo destr.\n"); } } try { $x = new Der; } catch (Exception $e) { } с class Base { public function __construct() { print("Base const.\n"); } public function __destruct() { print("Base destr.\n"); } } class Der extends Base { public function __construct() { print("Der const.\n"); parent::__construct(); $this->foo = new Foo; $this->foo->__ref = $this; # <-- make Base and Def __destructors active throw new Exception("foo"); unset($this->foo->__ref); } public function __destruct() { unset($this->foo); parent::__destruct(); print("Der destr.\n"); } public $foo; } class Foo { public function __construct() { print("Foo const.\n"); } public function __destruct() { print("Foo destr.\n"); } } try { $x = new Der; } catch (Exception $e) { } 

Одним из основных различий между C ++ и PHP является то, что в PHP конструкторы базового класса и деструкторы не вызываются автоматически. Это явно указано на странице руководства PHP для конструкторов и деструкторов :

Примечание . Родительские конструкторы не называются неявно, если дочерний класс определяет конструктор. Чтобы запустить родительский конструктор, требуется вызов parent :: __ construct () в дочернем конструкторе.

Подобно конструкторам, родительские деструкторы не будут называться неявным образом движком. Чтобы запустить родительский деструктор, нужно было бы явно вызвать parent :: __ destruct () в теле деструктора.

Таким образом, PHP оставляет задачу правильного вызова конструкторов и деструкторов базового класса целиком вплоть до программиста, и всегда обязательно, чтобы программист отвечал на вызов конструктора базового класса и деструктора, когда это необходимо.

При необходимости ключевой момент в вышеуказанном параграфе. Редко будет ситуация, когда неспособность вызвать деструктора будет «утечка ресурса». Имейте в виду, что члены данных базового экземпляра, созданного при вызове конструктора базового класса, сами станут непринятыми, поэтому будет вызван деструктор (если он существует) для каждого из этих членов. Попробуйте этот код:

 <?php class MyResource { function __destruct() { echo "MyResource::__destruct\n"; } } class Base { private $res; function __construct() { $this->res = new MyResource(); } } class Derived extends Base { function __construct() { parent::__construct(); throw new Exception(); } } new Derived(); 

Пример вывода:

 MyResource :: __ разрушиться

 Неустранимая ошибка: исключить исключение «Исключение» в /t.php:20
 Трассировки стека:
 # 0 /t.php(24): Derived -> __ construct ()
 # 1 {main}
   брошен в /t.php в строке 20

http://codepad.org/nnLGoFk1

В этом примере конструктор Derived вызывает конструктор Base , который создает новый экземпляр MyResource . Когда Derived впоследствии выдает исключение в конструкторе, экземпляр MyResource созданный конструктором Base становится неучтенным. В конце концов, будет MyResource деструктор MyResource .

Один сценарий, где может потребоваться вызвать деструктор, – это то, где деструктор взаимодействует с другой системой, такой как реляционная СУБД, кеш, система обмена сообщениями и т. Д. Если деструктор должен быть вызван, то вы можете либо инкапсулировать деструктор как отдельный объект, не затронутый иерархиями классов (как в примере выше с MyResource ), или используйте блок catch :

 class Derived extends Base { function __construct() { parent::__construct(); try { // The rest of the constructor } catch (Exception $ex) { parent::__destruct(); throw $ex; } } function __destruct() { parent::__destruct(); } } 

EDIT: Чтобы эмулировать очистку локальных переменных и членов данных самого производного класса, вам нужно иметь блок catch для очистки каждой локальной переменной или элемента данных, который успешно инициализирован:

 class Derived extends Base { private $x; private $y; function __construct() { parent::__construct(); try { $this->x = new Foo(); try { $this->y = new Bar(); try { // The rest of the constructor } catch (Exception $ex) { $this->y = NULL; throw $ex; } } catch (Exception $ex) { $thix->x = NULL; throw $ex; } } catch (Exception $ex) { parent::__destruct(); throw $ex; } } function __destruct() { $this->y = NULL; $this->x = NULL; parent::__destruct(); } } 

Так было и в Java, перед заявлением try-with-resources Java 7 .