在编写一段析构方法的研究代码中,我遇到了交叉知识点导致的错误——在不同作用域,析构方法与引用次数导致了不一样的结果。
前提
本文假装你已经明白什么是析构方法、作用域及引用次数。关于后者,引用次数是 PHP 垃圾收集中的重要机制,它很大程度上,帮助 PHP 在程序运行时清理内存垃圾(参考:引用计数基础 - PHPDoc)。
正文
有误的测试
来看这段代码:
class A {
public $var = [];
public function __construct()
{
echo '__construct: ' . spl_object_hash($this) . "\n";
}
public function __destruct()
{
echo '__destruct: ' . spl_object_hash($this);
}
public function test()
{
throw new Exception('Hello');
}
}
$test = new A();
$test->test();
我的本意是“在抛出不捕获的异常时,析构方法是否正常执行”。结果是没有执行,OK,很稳:
__construct: 0000000045f0af9e00000000494744b0
Fatal error: Uncaught Exception: Hello in...
当我们以为事情就此结束,后续往往会接踵而来。
翻车的代码
在公司前辈指出“你这段代码有问题,犯了作用域的错误”之后,我是当场宕机的。
啥,作用域?析构方法?我是不是听错了,那玩意不是变量的概念么。
经过我的追问,前辈告诉我:你把执行代码放到函数里试试。
避免文章过长,直接上差异部分的代码,如下:
class A {
// 与之前一致
}
function test()
{
$test = new A();
$test->test();
}
test();
结果如下:
__construct: 000000004b11d811000000006f9a75c7
__destruct: 000000004b11d811000000006f9a75c7
Fatal error: Uncaught Exception: Hello in...
心态如下:
说好的不执行呢?真是令人绝望。
当场打脸,只好去琢磨“析构方法的作用域”是个啥。搜索结果里看到了这样的话:
析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。来源: 构造与析构函数 - PHPDoc
让我们推理一下:
- 函数结束后,该函数级别的作用域就结束了,而此时脚本还未结束。没有任何引用的对象实例,自然可以执行析构方法;
- 全局作用域则不一样,所以导致了对象在全局作用域结束后,没机会调用析构方法。
结果似乎明朗了。
深入
当然,浅尝辄止可对不起我的好奇心。既然要搞明白这个问题,那就问一问核心问题:
- 证实:函数级别的作用域结束与对象执行析构方法,是否有必然联系?
- 新问题:调用析构方法与结束变量,谁先谁后?
相信在理清上述两个问题的答案后,这个文章也就没有存在的意义了,笑~
问题一
函数级别的作用域结束与对象执行析构方法,是否有必然联系?
很简单,咱们让对象与函数的作用域脱钩,就可以逆向地证实这一点:
class A {
// 与之前一致
}
$i = 123;
function test(&$i) // 通过引用机制,给函数的作用域增加污染变量
{
$test = new A();
$i = $test; // 将对象实例的引用扩展到全局作用域
$test->test();
}
test($i);
结果如下:
__construct: 0000000042a054c3000000001f53236f
Fatal error: Uncaught Exception: Hello in...
果然,当引用计数不为 0 时,析构函数就不会被调用,贼稳~
问题二
新问题:调用析构方法与结束变量,谁先谁后?
这个问题就有点意思了,熟悉程序的朋友又应该明白,遇到这种“X的某个机制是什么时候触发的”,就应该去看X的生命周期,X 泛指一切。
在经过一番查找,我从《PHP7内核剖析》中找到了 PHP 的生命周期,注意我标红圈的两个地方:
清理全局变量与析构方法的调用,我们就找到了。
但此时困惑了我的问题就变成了:普通变量到底什么时候销毁?
我翻遍了 PHP 的生命周期、网络上的文章,也没找到想要的答案。大家都在聊全局变量的销毁事件,难道全局的普通变量是弱势群体吗?
直到我看到 PHP 手册上的范例:
// 使用 global
$a = 1;
$b = 2;
function Sum()
{
global $a, $b;
$b = $a + $b;
}
Sum();
echo $b;
原来 全局范围的普通变量 = 全局变量,这结论真是令我头秃。
最终总结一下:
- 当实例的引用为 0 时,会步入销毁阶段,此时,析构函数才会启用;
- 当对象的实例位于全局作用域,该变量会在 全局变量销毁 事件中销毁,在此之前,全局变量的引用数至少为 1;
- 析构函数的调用 发生在 全局变量销毁 之前。
- 当析构函数的调用钩子去检测“引用数”时,全局的实例自然无法触发这个事件。
至于为什么会犯这样的错误,原因也有两个:
- 对 PHP 的生命周期认知模糊不清;
- 不清楚 PHP 的全局变量如何定义。
为什么会犯这两个错误,自然也有理由,但无论什么理由,都解决不了在面对知识点交叉时,因为知识盲点所犯下的错。下次学东西,还是跟着官方文档学习吧。
图片出处源自网络或水印,侵删。