__construct() 和 __destory() 在 PHP 中需要注意的地方

2020-07-16 10:05:50

基本上所有的程式語言在類中都會有建構函式和解構函式的概念。建構函式是在函數範例建立時可以用來做一些初始化的工作,而解構函式則可以在範例銷毀前做一些清理工作。相對來說,建構函式我們使用得非常多,而解構函式則一般會用在釋放資源上,比如資料庫連結、檔案讀寫的控制代碼等。

建構函式與解構函式的使用

我們先來看看正常的構造與解構函式的使用:

class A
{
    public $name;
    public function __construct($name)
    {
        $this->name = $name;
        echo "A:建構函式被呼叫,{$this->name}", PHP_EOL;
    }
    public function __destruct()
    {
        echo "A:解構函式被呼叫,{$this->name}", PHP_EOL;
    }
}
$a = new A('$a');
echo '-----', PHP_EOL;
class B extends A
{
    public function __construct($name)
    {
        $this->name = $name;
        parent::__construct($name);
        echo "B:建構函式被呼叫,{$this->name}", PHP_EOL;
    }
    public function __destruct()
    {
        parent::__destruct();
        echo "B:解構函式被呼叫,{$this->name}", PHP_EOL;
    }
}
class C extends A
{
    public function __construct($name)
    {
        $this->name = $name;
        echo "C:建構函式被呼叫,{$this->name}", PHP_EOL;
    }
    public function __destruct()
    {
        echo "C:解構函式被呼叫,{$this->name}", PHP_EOL;
    }
}
class D extends A
{
}
// unset($a); // $a的解構提前
// $a = null; // $a的解構提前
$b = new B('$b');
$c = new C('$c');
$d = new D('$d');
echo '-----', PHP_EOL;exit;
// A:建構函式被呼叫,$a
// -----
// A:建構函式被呼叫,$b
// B:建構函式被呼叫,$b
// C:建構函式被呼叫,$c
// A:建構函式被呼叫,$d
// -----
// A:解構函式被呼叫,$d
// C:解構函式被呼叫,$c
// A:解構函式被呼叫,$b
// B:解構函式被呼叫,$b
// A:解構函式被呼叫,$a

上面的程式碼是不是有一些內容和我們的預期不太一樣?沒事,我們一個一個來看:

子類如果重寫了父類別的構造或解構函式,如果不顯式地使用parent::__constuct()呼叫父類別的建構函式,那麼父類別的建構函式不會執行,如C類子類如果沒有重寫構造或解構函式,則預設呼叫父類別的解構函式如果沒顯式地將變數置為NULL或者使用unset()的話,會在指令碼執行完成後進行呼叫,呼叫順序在測試程式碼中是類似於棧的形式先進後出(C->B->A,C先被解構),但在伺服器環境中則不一定,也就是說順序不一定固定

解構函式的參照問題

當物件中包含自身相互的參照時,想要通過設定為NULL或者unset()來呼叫解構函式可能會出現問題。

class E
{
    public $name;
    public $obj;
    public function __destruct()
    {
        echo "E:解構函式被呼叫," . $this->name, PHP_EOL;
        echo '-----', PHP_EOL;
    }
}
$e1 = new E();
$e1->name = 'e1';
$e2 = new E();
$e2->name = 'e2';
$e1->obj = $e2;
$e2->obj = $e1;

類似於這樣的程式碼,$e1和$e2都是E類的物件,他們又各自持有對方的參照。其實簡單點來說的話,自己持有自己的參照都會出現類似的問題。

$e1 = new E();
$e1->name = 'e1';
$e2 = new E();
$e2->name = 'e2';
$e1->obj = $e2;
$e2->obj = $e1;
$e1 = null;
$e2 = null;
// gc_collect_cycles();
$e3 = new E();
$e3->name = 'e3';
$e4 = new E();
$e4->name = 'e4';
$e3->obj = $e4;
$e4->obj = $e3;
$e3 = null;
$e4 = null;
echo 'E destory', PHP_EOL;

如果我們不開啟gc_collect_cycles()那一行的注釋,解構函式執行的順序是這樣的:

// 不使用gc回收的結果
// E destory
// E:解構函式被呼叫,e1
// -----
// E:解構函式被呼叫,e2
// -----
// E:解構函式被呼叫,e3
// -----
// E:解構函式被呼叫,e4
// -----

如果我們開啟了gc_collect_cycles()的註釋,解構函式的執行順序是:

// 使用gc回收後結果
// E:解構函式被呼叫,e1
// -----
// E:解構函式被呼叫,e2
// -----
// E destory
// E:解構函式被呼叫,e3
// -----
// E:解構函式被呼叫,e4
// -----

可以看出,必須要讓php使用gc回收一次,確定物件的參照都被釋放了之後,類的解構函式才會被執行。參照如果沒有釋放,解構函式是不會執行的。

建構函式的低版本相容問題

在PHP5以前,PHP的建構函式是與類名同名的一個方法。也就是說如果我有一個F類,那麼function F(){}方法就是它的建構函式。為了向低版本相容,PHP依然保留了這個特性,在PHP7以後如果有與類名同名的方法,就會報過時警告,但不會影響程式執行。

class F
{
    public function f() 
    {
        // Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; F has a deprecated constructor 
        echo "F:這也是建構函式,與類同名,不區分大小寫", PHP_EOL;
    }
    // function F(){
    //     // Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; F has a deprecated constructor 
    //     echo "F:這也是建構函式,與類同名", PHP_EOL;
    // }
    // function __construct(){
    //     echo "F:這是建構函式,__construct()", PHP_EOL;
    // }
}
$f = new F();

如果__construc()和類同名方法同時存在的話,會優先走__construct()。另外需要注意的是,函數名不區分大小寫,所以F()和f()方法是一樣的都會成為建構函式。同理,因為不區分大小寫,所以f()和F()是不能同時存在的。當然,我們都不建議使用類同名的函數來做為建構函式,畢竟已經是過時的特性了,說不定哪天就被取消了。

建構函式過載

PHP是不執行方法的過載的,只支援重寫,就是子類重寫父類別方法,但不能定義多個同名方法而引數不同。在Java等語言中,過載方法非常方便,特別是在類範例化時,可以方便地實現多型能力。

$r1 = new R(); // 預設建構函式
$r2 = new R('arg1'); // 預設建構函式 一個引數的建構函式過載,arg1
$r3 = new R('arg1', 'arg2'); // 預設建構函式 兩個引數的建構函式過載,arg1,arg2

就像上述程式碼一樣,如果你嘗試定義多個__construct(),PHP會很直接地告訴你執行不了。那麼有沒有別的方法實現上述程式碼的功能呢?當然有,否則咱也不會寫了。

class R
{
    private $a;
    private $b;
    public function __construct()
    {
        echo '預設建構函式', PHP_EOL;
        $argNums = func_num_args();
        $args = func_get_args();
        if ($argNums == 1) {
            $this->constructA(...$args);
        } elseif ($argNums == 2) {
            $this->constructB(...$args);
        }
    }
    public function constructA($a)
    {
        echo '一個引數的建構函式過載,' . $a, PHP_EOL;
        $this->a = $a;
    }
    public function constructB($a, $b)
    {
        echo '兩個引數的建構函式過載,' . $a . ',' . $b, PHP_EOL;
        $this->a = $a;
        $this->b = $b;
    }
}
$r1 = new R(); // 預設建構函式
$r2 = new R('arg1'); // 預設建構函式 一個引數的建構函式過載,arg1
$r3 = new R('arg1', 'arg2'); // 預設建構函式 兩個引數的建構函式過載,arg1,arg2

相對來說比Java之類的語言要麻煩一些,但是也確實是實現了相同的功能哦。

建構函式和解構函式的存取限制

建構函式和解構函式預設都是public的,和類中的其他方法預設值一樣。當然它們也可以設定成private和protected。如果將建構函式設定成非公共的,那麼你將無法範例化這個類。這一點在單例模式被廣泛應用,下面我們直接通過一個單例模式的程式碼看來。

class Singleton
{
    private static $instance;
    public static function getInstance()
    {
        return self::$instance == null ? self::$instance = new Singleton() : self::$instance;
    }
    private function __construct()
    {
    }
}
$s1 = Singleton::getInstance();
$s2 = Singleton::getInstance();
echo $s1 === $s2 ? 's1 === s2' : 's1 !== s2', PHP_EOL;
// $s3 = new Singleton(); // Fatal error: Uncaught Error: Call to private Singleton::__construct() from invalid context

當$s3想要範例化時,直接就報錯了。關於單例模式為什麼要讓外部無法範例化的問題,我們可以看看之前的設計模式系統文章中的單例模式。

總結

沒想到我們天天用到的建構函式還能玩出這麼多花樣來吧,日常在開發中比較需要注意的就是子類繼承時對建構函式重寫時父類別建構函式的呼叫問題以及參照時的解構問題。

以上就是__construct() 和 __destory() 在 PHP 中需要注意的地方的詳細內容,更多請關注TW511.COM其它相關文章!