10

我正在学习 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.

另一方面,如果构造函数(at )中存在异常,则成员对象的析构函数会正确执行#1现在我想知道:如何在 PHP 的类层次结构中实现正确的范围展开,以便在发生异常时正确销毁子对象?

此外,似乎没有办法在所有成员对象都被销毁(at )之后#2运行基本析构函数。也就是说,如果我们删除 line #1,我们会得到:

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

如何解决这个问题?

更新:我仍然愿意接受进一步的贡献。如果有人有充分的理由说明为什么 PHP 对象系统从不需要正确的销毁序列,我将为此提供另一个赏金(或只是为任何其他令人信服的争论答案)。

4

4 回答 4

6

我想解释为什么 PHP 会以这种方式运行,以及为什么它实际上(某些)有意义。

在 PHP中,一旦不再有对它的引用,对象就会被销毁。可以通过多种方式删除引用,例如通过unset()变量、离开范围或作为关闭的一部分。

如果你理解了这一点,你就可以很容易地理解这里发生了什么(我将首先解释没有异常的情况):

  1. PHP 进入关闭状态,因此所有变量引用都被删除。
  2. $x(对 的实例Der)创建的引用被删除时,对象被销毁。
  3. 派生的析构函数被调用,它调用基析构函数。
  4. 现在对实例的引用$this->fooFoo删除(作为破坏成员字段的一部分。)
  5. 没有更多的引用Foo,所以它也被销毁并调用了析构函数。

想象一下这不会以这种方式工作,并且成员字段将在调用析构函数之前被销毁:您无法再在析构函数中访问它们。我严重怀疑 C++ 中是否存在这种行为。

在 Exception 情况下,您需要了解对于 PHP,实际上从来没有存在过该类的实例,因为构造函数从未返回。你怎么能破坏从未建造过的东西?


我如何解决它?

你没有。您需要析构函数这一事实可能是糟糕设计的标志。破坏令对你来说非常重要,这一事实甚至更重要。

于 2011-09-23T06:42:55.310 回答
2

这不是答案,而是对问题动机的更详细解释。我不想用这种有点切线的材料来混淆问题本身。

这是我如何期望具有成员的派生类的通常破坏顺序的解释。假设类是这样的:

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

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

当我创建一个实例时$z = new Derived;,那么 this 首先构造子对象,然后是(即)Base的成员对象,最后是执行的构造函数。Derived$z->fooDerived

因此,我希望破坏顺序以完全相反的顺序发生:

  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's 的析构函数将访问一个无效的引用。

于 2011-09-23T13:48:17.457 回答
1

C++ 和 PHP 之间的一个主要区别是,在 PHP 中,不会自动调用基类构造函数和析构函数。这在Constructors 和 Destructors 的 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

致命错误:/t.php:20 中未捕获的异常“异常”
堆栈跟踪:
#0 /t.php(24): 派生->__construct()
#1 {主要}
  在第 20 行的 /t.php 中抛出

http://codepad.org/nnLGoFk1

在此示例中,Derived构造函数调用Base构造函数,该构造函数创建一个新MyResource实例。当Derived随后在构造函数中抛出异常时,构造函数MyResource创建的实例将Base变为未引用。最终,MyResource将调用析构函数。

可能需要调用析构函数的一种情况是析构函数与另一个系统交互,例如关系 DBMS、缓存、消息传递系统等。如果必须调用析构函数,那么您可以将析构函数封装为单独的不受类层次结构影响的对象(如上例中的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 7 的 try-with-resources 语句之前,这也是 Java 中的做法。

于 2011-09-29T13:21:15.057 回答
1

如果在构造函数中抛出异常,则对象永远不会存在(对象的 zval 至少有一个引用计数,这是析构函数所需要的),因此没有可以调用的析构函数。

现在我想知道:如何在 PHP 的类层次结构中实现正确的范围展开,以便在发生异常时正确销毁子对象?

在你给出的例子中,没有什么可以放松的。但是对于游戏,让我们假设,您知道基础构造函数可以抛出异常,但您需要$this->foo在调用它之前进行初始化。

然后,您只需要$this(暂时)将“”的引用计数提高一个,这需要(一点)不仅仅是一个局部变量 in __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) {
}
于 2011-09-28T20:30:45.473 回答