我正在学习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()
变量,通过离开作用域或作为关闭的一部分。
如果你理解了这一点,你就可以很容易地理解这里发生的事情(我将首先解释没有Exception的情况):
- PHP进入关闭状态,因此所有变量引用都被删除。
- 当
$x
(Der
的实例)创建的引用被删除时,对象被销毁。 - 调用派生析构函数,它调用基析构函数。
- 现在从
$this->foo
到Foo
实例的引用被删除了(作为销毁成员字段的一部分) - 也没有任何对
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
的构造函数。
因此,我期望销毁顺序以完全相反的顺序发生:
execute
Derived
destructor销毁
Derived
的成员对象execute
Base
destructor.
然而,由于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
。然后使用对important_resource
的引用初始化成员对象rc
,这在rc
销毁期间是必需的。因此,当x
的生命周期结束时,首先调用派生析构函数(什么都不做),然后销毁rc
,完成它的清理工作,只有然后被销毁Base
子对象,释放important_resource
。
如果析构函数顺序错误,则rc
的析构函数访问的是无效引用
如果在构造函数内部抛出异常,则对象永远不会激活(对象的zval至少有一个引用计数为1,这是析构函数所需要的),因此没有任何具有可调用的析构函数。
现在我想知道:如何在PHP的类层次结构中实现正确的范围展开,以便在发生异常时正确地销毁子对象?
在你给出的例子中,没有什么需要unwind的。但是对于游戏,让我们假设,你知道基本构造函数可以抛出异常,但是你需要在调用它之前初始化$this->foo
。
然后您只需要将"$this
"的refcount(暂时)提高1,这需要(稍微)超过__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析构函数时的顺序,在析构函数中取消该属性,如下例所示。
Edit:正如您可以控制对象构造的时间一样,您也可以控制对象何时被销毁。以下顺序:
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) {
}
c++和PHP的一个主要区别是,在PHP中,基类构造函数和析构函数不是自动调用的。这在PHP手册的构造函数和析构函数页面中有明确提到:
因此,PHP将正确调用基类构造函数和析构函数的任务完全交给程序员,而在必要时调用基类构造函数和析构函数始终是程序员的责任。注意:如果子类定义了构造函数,则不会隐式调用父构造函数。为了运行父构造函数,需要在子构造函数中调用parent::__construct()。
…与构造函数一样,父类析构函数不会被引擎隐式调用。为了运行父类析构函数,必须在析构函数体中显式调用parent::__destruct()。
上述段落的重点是必要时。很少会出现未能调用析构函数会"泄漏资源"的情况。请记住,在调用基类构造函数时创建的基实例的数据成员本身将变为未引用,因此将调用每个成员的析构函数(如果存在的话)。试试下面的代码:
<?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致命错误:未捕获的异常' exception '在/t.p php:20堆栈跟踪:#0/t.p php(24): Derived->__construct()# 1{主要}在/t.p php第20行抛出之前http://codepad.org/nnLGoFk1 在这个例子中,Derived
构造函数调用Base
构造函数,后者创建了一个新的MyResource
实例。当Derived
随后在构造函数中抛出异常时,由Base
构造函数创建的MyResource
实例变为未引用。最后,将调用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();
}
}
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();
}
}