Разматывание области в конструкторах класса PHP - PullRequest
10 голосов
/ 19 сентября 2011

Я изучаю 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 никогда не требует правильной последовательности уничтожения , я выдам еще одну награду за это (или просто за любой другой убедительно аргументированный ответ).

Ответы [ 4 ]

6 голосов
/ 23 сентября 2011

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

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

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

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

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

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


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

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

2 голосов
/ 23 сентября 2011

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

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

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

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

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

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

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

1 голос
/ 29 сентября 2011

Одним из основных отличий между 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::__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();
    }
}

РЕДАКТИРОВАТЬ: Чтобы эмулировать очистку локальных переменных и членов данных самого производного класса, вам нужен блок 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 .

1 голос
/ 29 сентября 2011

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

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

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

Затем вам нужно всего лишь увеличить (до) (1) рефконт "$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) {
}
...