资讯专栏INFORMATION COLUMN

【In PHP】析构、作用域与引用

fai1017 / 1309人阅读

摘要:在编写一段析构方法的研究代码中,我遇到了交叉知识点导致的错误在不同作用域,析构方法与引用次数导致了不一样的结果。前提本文假装你已经明白什么是析构方法作用域及引用次数。当析构函数的调用钩子去检测引用数时,全局的实例自然无法触发这个事件。

在编写一段析构方法的研究代码中,我遇到了交叉知识点导致的错误——在不同作用域,析构方法与引用次数导致了不一样的结果。
前提

本文假装你已经明白什么是析构方法、作用域及引用次数。关于后者,引用次数是 PHP 垃圾收集中的重要机制,它很大程度上,帮助 PHP 在程序运行时清理内存垃圾(参考:引用计数基础 - PHPDoc)。

正文 有误的测试

来看这段代码:

class A {
    public $var = [];

    public function __construct()
    {
        echo "__construct: " . spl_object_hash($this) . "
";
    }

    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 的全局变量如何定义。

为什么会犯这两个错误,自然也有理由,但无论什么理由,都解决不了在面对知识点交叉时,因为知识盲点所犯下的错。下次学东西,还是跟着官方文档学习吧。


图片出处源自网络或水印,侵删。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/29766.html

相关文章

  • PHP面试之面向对象(1)

    摘要:二面向对象有什么特征面向对象的主要特征有抽象继承封装和多态。析构函数析构函数是在引入的,它的作用与调用时机和构造函数刚好相反,它在对象被销毁时自动执行。 PHP面试专栏正式起更,每周一、三、五更新,提供最好最优质的PHP面试内容。PHP中面向对象常考的知识点有以下7点,我将会从以下几点进行详细介绍说明,帮助你更好的应对PHP面试常考的面向对象相关的知识点和考题。整个面向对象文章的结构涉...

    phodal 评论0 收藏0
  • 重读PHP手册笔记系列(二)

    摘要:通过注册自动加载器,脚本引擎在出错失败前有了最后一个机会加载所需的类构造函数和析构函数构造函数允行开发者在一个类中定义一个方法作为构造函数。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行抽象类支持抽象类和抽象方法。 语言参考 1. 类型 1.1 简介 showImg(https://segmentfault.com/img/bVbqlNJ?w=531&h=379);...

    马龙驹 评论0 收藏0
  • php易错笔记-类与对象,命名空间

    摘要:类与对象基本概念如果在之后跟着的是一个包含有类名的字符串,则该类的一个实例被创建。如果该类属于一个名字空间,则必须使用其完整名称。如果一个类被声明为,则不能被继承。命名空间通过关键字来声明。 类与对象 基本概念 new:如果在 new 之后跟着的是一个包含有类名的字符串,则该类的一个实例被创建。如果该类属于一个名字空间,则必须使用其完整名称。 Example #3 创建一个实例 ...

    MartinHan 评论0 收藏0
  • PHP面试常考内容之面向对象(2)

    摘要:继上一篇面试常考内容之面向对象发表后,今天更新,需要的可以直接点击文字进行跳转获取。析构函数,当对象被销毁时调用。 PHP面试专栏正式起更,每周一、三、五更新,提供最好最优质的PHP面试内容。继上一篇PHP面试常考内容之面向对象(1)发表后,今天更新(2),需要(1)的可以直接点击文字进行跳转获取。整个面向对象文章的结构涉及的内容模块有: 一、面向对象与面向过程有什么区别?二、面向对...

    Barry_Ng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<