【In PHP】析構、做用域與引用

在編寫一段析構方法的研究代碼中,我遇到了交叉知識點致使的錯誤——在不一樣做用域,析構方法與引用次數致使了不同的結果。

前提

本文僞裝你已經明白什麼是析構方法、做用域及引用次數。關於後者,引用次數是 PHP 垃圾收集中的重要機制,它很大程度上,幫助 PHP 在程序運行時清理內存垃圾(參考:引用計數基礎 - PHPDoc)。php

正文

有誤的測試

來看這段代碼:html

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...

當咱們覺得事情就此結束,後續每每會接踵而來。函數

翻車的代碼

在公司前輩指出「你這段代碼有問題,犯了做用域的錯誤」以後,我是當場宕機的。oop

震驚
啥,做用域?析構方法?我是否是聽錯了,那玩意不是變量的概念麼。學習

通過個人追問,前輩告訴我:你把執行代碼放到函數裏試試。測試

避免文章過長,直接上差別部分的代碼,以下:this

class A {
    // 與以前一致
}

function test()
{
    $test = new A();
    $test->test();
}

test();

結果以下:spa

__construct: 000000004b11d811000000006f9a75c7
__destruct: 000000004b11d811000000006f9a75c7
Fatal error: Uncaught Exception: Hello in...

心態以下:.net

哭笑
說好的不執行呢?真是使人絕望。

當場打臉,只好去琢磨「析構方法的做用域」是個啥。搜索結果裏看到了這樣的話:

析構函數會在到某個對象的全部引用都被刪除或者當對象被顯式銷燬時執行。來源: 構造與析構函數 - 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 的生命週期、網絡上的文章,也沒找到想要的答案。你們都在聊全局變量的銷燬事件,難道全局的普通變量是弱勢羣體嗎?

直到我看到 PHP 手冊上的範例

// 使用 global

$a = 1;
$b = 2;

function Sum()
{
    global $a, $b;

    $b = $a + $b;
}

Sum();
echo $b;

原來 全局範圍的普通變量 = 全局變量,這結論真是令我頭禿。

最終總結一下:

  1. 當實例的引用爲 0 時,會步入銷燬階段,此時,析構函數纔會啓用;
  2. 當對象的實例位於全局做用域,該變量會在 全局變量銷燬 事件中銷燬,在此以前,全局變量的引用數至少爲 1;
  3. 析構函數的調用 發生在 全局變量銷燬 以前。
  4. 當析構函數的調用鉤子去檢測「引用數」時,全局的實例天然沒法觸發這個事件。

至於爲何會犯這樣的錯誤,緣由也有兩個:

  1. 對 PHP 的生命週期認知模糊不清;
  2. 不清楚 PHP 的全局變量如何定義。

爲何會犯這兩個錯誤,天然也有理由,但不管什麼理由,都解決不了在面對知識點交叉時,由於知識盲點所犯下的錯。下次學東西,仍是跟着官方文檔學習吧。


圖片出處源自網絡或水印,侵刪。

相關文章
相關標籤/搜索