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

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

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

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.

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

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

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

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

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

Ответ 1

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

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

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

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

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

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


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

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

Ответ 2

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

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

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

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

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

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

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

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

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

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

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

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

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

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 имел бы доступ к недопустимой ссылке.

Ответ 3

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

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

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

Вам нужно только поднять refcount из "$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
  }

Результат:

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) {
}

Ответ 4

Одно из основных различий между С++ и 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::__destruct

Fatal error: Uncaught exception 'Exception' in /t.php:20
Stack trace:
#0 /t.php(24): Derived->__construct()
#1 {main}
  thrown in /t.php on line 20

http://codepad.org/nnLGoFk1

В этом примере конструктор Derived вызывает конструктор Base, который создает новый экземпляр MyResource. Когда Derived впоследствии выдает исключение в конструкторе, экземпляр MyResource, созданный конструктором Base, становится неотображенным. В конце концов вызывается деструктор 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, прежде чем Java 7 try-with-resources statement.