PHP类构造函数中的范围展开

时间:2011-09-19 12:31:54

标签: php exception stack-unwinding

我正在学习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 个答案:

答案 0 :(得分:6)

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

在PHP 中,只要没有对象的引用,就会销毁该对象。可以以多种方式移除参考,例如,通过unset()变量,保留范围或作为关闭的一部分。

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

  1. PHP进入关闭状态,因此删除了所有变量引用。
  2. $x创建的引用(对Der的实例)被删除时,对象将被销毁。
  3. 调用派生的析构函数,它调用基础析构函数。
  4. 现在删除了从$this->fooFoo实例的引用(作为销毁成员字段的一部分。)
  5. 也没有对Foo的任何引用,因此它也被销毁并且析构函数被调用。
  6. 想象一下,这不会以这种方式工作,并且会在调用析构函数之前销毁成员字段:您无法在析构函数中访问它们。我严重怀疑在C ++中存在这样的行为。

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


    如何解决?

    你没有。你需要一个析构函数这一事实可能是设计糟糕的表现。而破坏令对你来说非常重要的事实甚至更多。

答案 1 :(得分:2)

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

这是一个解释,我将如何期望具有成员的派生类的通常销毁序列。假设这个类是这样的:

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. 的成员对象
  4. 执行Base析构函数。

  5. 但是,由于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,执行清理作业,只有然后是{ {1}}子对象被销毁,释放Base

    如果破坏是乱序发生的,那么important_resource的析构函数就会访问无效的引用。

答案 2 :(得分:1)

如果在构造函数中抛出一个异常,该对象永远不会生效(对象的zval至少有一个引用计数为1,这是析构函数所需的),因此没有任何东西可以使用析构函数被称为。

  

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

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

然后你只需要将“$this”的引用数量提高一个(暂时),这需要(一点点)比__construct中的局部变量更多,让我们把它变成$foo。 1}}本身:

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.

Demo

如果您需要此功能,请考虑自己。

要在调用Foo析构函数时控制顺序,请在析构函数中取消设置属性,例如this example demonstrates

编辑:由于您可以控制构造对象的时间,因此您可以控制何时对象被销毁。以下顺序:

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

答案 3 :(得分:1)

C ++和PHP之间的一个主要区别是,在PHP中,基类构造函数和析构函数不会自动调用。这在the PHP Manual page for Constructors and Destructors上明确提到:

  

注意:如果子类定义了构造函数,则不会隐式调用父构造函数。为了运行父构造函数,需要在子构造函数中调用 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析构函数。

可能需要调用析构函数的一种情况是析构函数与另一个系统交互,例如关系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's try-with-resources statement之前在Java中完成的。