PHP析构函数被调用两次

3
以下代码示例中,destruct()被调用了两次。我想知道为什么?
class A {
    function hi(){ echo 'hi'; }
    function __destruct(){
        echo 'destroy';
    }
}
class B{
    public $this_ = '';
    function __construct(){
        $this->this_ = new A;
    }
    function __call($method, $params) {
          return call_user_func_array(array($this->this_, $method), $params);
    }
}

$b = new B;
$b->__destruct();

输出:

destroydestroy

编辑

zneak和TomcatExodus都是正确的。如果我只是:

[..code..]
$b = new B;
$b->__destruct();
print 'end of script';

输出将显示:
destroyend of scriptdestroy

哦,我没有注意到B没有扩展A。我认为它可能会调用您在其上调用的实例中包含的每个对象的析构函数。 - BoltClock
@TomcatExodus:这不是打字错误。OP说在B上调用destruct会在A属性上调用它。 - BoltClock
@BoltClock; 是的,我已经意识到并删除了评论。刚刚测试了一下,由于类 B 创建了类 A 的一个实例,并且 B 也使用 __call() 来路由到自包含的 A 对象,所以对 B 调用 __destruct() 会被路由到自包含的 A 实例。在脚本终止时,所有对象都离开内存,A 对象再次触发析构函数。我相信我们的答案仍然是正确的,只是情况有些扭曲。 - Dan Lugg
1
@TomcatExodus:该死的魔术方法! :) - BoltClock
4个回答

14

__destruct() 方法的调用并不会销毁对象。第一次需要使用 __destruct() 调用它,然后当 PHP 脚本终止时,会在清理过程中再次调用它。

如果你想在脚本终止之前销毁该对象,请使用 unset()。这样你只会看到一个 __destruct() 调用。


具体来说,你的类 B 创建了一个包含类 A 的实例。由于 B 通过 __call() 将方法调用路由到 A 对象,所以在 B 上调用 __destruct() 也会导致 A 上的 __destruct() 被调用;因为 B 没有定义析构函数并将调用传递给上一级。


是的,我错过了几个,但已经添加到注释中了,我也会在答案中添加。 - Dan Lugg
@BoltClock;谢谢!不客气,精确是必要的 :) - Dan Lugg

10

因为 B 没有 __destruct 方法,所以会调用 __call 方法(你可以在你的 __call 方法中添加类似 echo "calling $method" 的内容进行验证),然后再转发到你的 A 对象。

但是,调用 __destruct 并不会销毁对象:它只是调用应该与其销毁相关联的清理代码。因此,一旦你到达脚本的末尾,实际上销毁了 A 对象时,它的 __destruct 方法会再次被调用。

如果你想删除你的 B 对象,请使用 unset($b)


我不知道该从你们两个人中正式宣布哪一个是正确的答案,TomcatExodus和zneak,因为你们两个都是正确的。在调用__destruct()后简单测试打印一些内容后,显示类的销毁在页面执行后再次调用__destruct(),导致打印出“destructend of scriptdestruct”。 - Paul Carlton
@Xiquid,请将正确答案标记给TomcatExodus,我在他之后几分钟才回答,而他比我更需要声望。 - zneak
还有一个问题,你们都说unset($b),但是在阅读相关资料时,发现unset()并不会释放资源,最好的方法是先调用__destruct()再unset(),这可能是我有点困惑的原因:http://www.php.net/manual/en/function.unset.php#98692 - Paul Carlton
1
@Xiquid 我很确定这是错误的。你可以通过一个简单的代码片段进行测试,该代码片段创建然后unset一个对象。在析构函数中放置一个echo语句,在unset调用之后再放置另一个echo语句,你会发现__destructunset调用期间确实被调用了(与那个人所说的相反)。唯一不会发生的情况是如果对象仍然有其他引用(例如$a = new A; $b = $a; unset($a);将不会调用析构函数,因为$b仍然引用该对象)。 - zneak

2
手动调用析构函数是最糟糕的想法之一,特别是当处理其他人的代码时。一个对象具有从构造函数开始、通过方法进行和以析构函数结束的逻辑。在析构函数上,对象可能需要一些清理工作,并且变量可能会失效。当析构函数由于成功的unset($Object)而被内部调用时,对象将不再可用。如果您手动执行它,则可以访问该对象,但如果对象自己进行了某些清理,则无法支持内部变量。

现在想象一下,如果您在手动调用析构函数后,对依赖您已经过早失效的数据的对象调用方法,会出现什么情况。它会破坏整个逻辑!因此,请始终使用unset()并让PHP完成其工作。

如果您知道自己在做什么,手动调用析构函数(placement delete :))在C++中非常好用,尤其是与放置new结合使用。但是,在整个实现过程中,您必须保护自己并确保在调用方法时实际上拥有数据。如果手动管理内存,则在析构函数中也必须保护自己,以免重复删除指针并在此过程中崩溃。

C++中的内存管理非常酷!我讨厌GC(垃圾收集器) :)

---发泄结束---


1
嗨@CodeAngry!感谢你的发泄。 :)是的,我理解这一点。调用__destruct()没有意义。当我使用类包装器时,我总是在内部取消设置我的对象。感谢你在C++中的内存管理提示。 - Paul Carlton

0

您正在手动调用一个析构函数;但这并不意味着您正在删除该对象。您只是在调用一个方法,而这个方法就像任何其他方法一样。

调用$b->__destruct()会调用$b->this_的析构函数,因为$b没有显式的析构函数方法。

当脚本结束时,Zend引擎会调用所有实例化对象的析构函数,然后执行例行清理操作,其中涉及调用包含对象的析构函数,即销毁$b后,必须清除$b->this_,为此引擎会自动调用其构造函数。

请注意,第二次调用不是由于销毁$b,而是由于销毁A实例。

手动销毁对象没有缺陷,并且它确实释放其资源(除非该对象正在共享,否则GC不会销毁它,除非没有对它的更多引用;在PHP中没有弱引用)。

垃圾回收机制的工作示例: http://codepad.org/7JDBoOKY

对象在代码完成之前被销毁。如果不是这样,输出顺序将会颠倒。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接