構造函數以及析構函數在PHP中須要注意的地方

基本上全部的編程語言在類中都會有構造函數和析構函數的概念。構造函數是在函數實例建立時能夠用來作一些初始化的工做,而析構函數則能夠在實例銷燬前作一些清理工做。相對來講,構造函數咱們使用得很是多,而析構函數則通常會用在釋放資源上,好比數據庫連接、文件讀寫的句柄等。php

構造函數與析構函數的使用

咱們先來看看正常的構造與析構函數的使用:git

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
複製代碼

上面的代碼是否是有一些內容和咱們的預期不太同樣?沒事,咱們一個一個來看:github

  • 子類若是重寫了父類的構造或析構函數,若是不顯式地使用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回收一次,肯定對象的引用都被釋放了以後,類的析構函數纔會被執行。引用若是沒有釋放,析構函數是不會執行的。bash

構造函數的低版本兼容問題

在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想要實例化時,直接就報錯了。關於單例模式爲何要讓外部沒法實例化的問題,咱們能夠看看以前的設計模式系統文章中的單例模式

總結

沒想到咱們每天用到的構造函數還能玩出這麼多花樣來吧,平常在開發中比較須要注意的就是子類繼承時對構造函數重寫時父類構造函數的調用問題以及引用時的析構問題。

測試代碼:github.com/zhangyue050…

參考文檔: www.php.net/manual/zh/l… www.php.net/manual/zh/l… www.php.net/manual/zh/l… www.php.net/manual/zh/l…



===============

關注公衆號:【硬核項目經理】獲取最新文章

添加微信/QQ好友:【DarkMatterZyCoder/149844827】免費得PHP、項目管理學習資料
相關文章
相關標籤/搜索