構(gòu)造函數(shù)以及析構(gòu)函數(shù)在PHP中需要注意的地方
基本上所有的編程語言在類中都會有構(gòu)造函數(shù)和析構(gòu)函數(shù)的概念,。構(gòu)造函數(shù)是在函數(shù)實例創(chuàng)建時可以用來做一些初始化的工作,而析構(gòu)函數(shù)則可以在實例銷毀前做一些清理工作,。相對來說,,構(gòu)造函數(shù)我們使用得非常多,而析構(gòu)函數(shù)則一般會用在釋放資源上,比如數(shù)據(jù)庫鏈接,、文件讀寫的句柄等,。
構(gòu)造函數(shù)與析構(gòu)函數(shù)的使用
我們先來看看正常的構(gòu)造與析構(gòu)函數(shù)的使用:
class A
{
public $name;
public function __construct($name)
{
$this->name = $name;
echo "A:構(gòu)造函數(shù)被調(diào)用,{$this->name}", PHP_EOL;
}
public function __destruct()
{
echo "A:析構(gòu)函數(shù)被調(diào)用,,{$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:構(gòu)造函數(shù)被調(diào)用,,{$this->name}", PHP_EOL;
}
public function __destruct()
{
parent::__destruct();
echo "B:析構(gòu)函數(shù)被調(diào)用,{$this->name}", PHP_EOL;
}
}
class C extends A
{
public function __construct($name)
{
$this->name = $name;
echo "C:構(gòu)造函數(shù)被調(diào)用,,{$this->name}", PHP_EOL;
}
public function __destruct()
{
echo "C:析構(gòu)函數(shù)被調(diào)用,,{$this->name}", PHP_EOL;
}
}
class D extends A
{
}
// unset($a); // $a的析構(gòu)提前
// $a = null; // $a的析構(gòu)提前
$b = new B('$b');
$c = new C('$c');
$d = new D('$d');
echo '-----', PHP_EOL;exit;
// A:構(gòu)造函數(shù)被調(diào)用,$a
// -----
// A:構(gòu)造函數(shù)被調(diào)用,,$b
// B:構(gòu)造函數(shù)被調(diào)用,,$b
// C:構(gòu)造函數(shù)被調(diào)用,$c
// A:構(gòu)造函數(shù)被調(diào)用,,$d
// -----
// A:析構(gòu)函數(shù)被調(diào)用,,$d
// C:析構(gòu)函數(shù)被調(diào)用,$c
// A:析構(gòu)函數(shù)被調(diào)用,,$b
// B:析構(gòu)函數(shù)被調(diào)用,$b
// A:析構(gòu)函數(shù)被調(diào)用,,$a
上面的代碼是不是有一些內(nèi)容和我們的預期不太一樣,?沒事,我們一個一個來看:
- 子類如果重寫了父類的構(gòu)造或析構(gòu)函數(shù),,如果不顯式地使用parent::__constuct()調(diào)用父類的構(gòu)造函數(shù),,那么父類的構(gòu)造函數(shù)不會執(zhí)行,如C類
- 子類如果沒有重寫構(gòu)造或析構(gòu)函數(shù),,則默認調(diào)用父類的
- 析構(gòu)函數(shù)如果沒顯式地將變量置為NULL或者使用unset()的話,,會在腳本執(zhí)行完成后進行調(diào)用,調(diào)用順序在測試代碼中是類似于棧的形式先進后出(C->B->A,,C先被析構(gòu)),,但在服務器環(huán)境中則不一定,也就是說順序不一定固定
析構(gòu)函數(shù)的引用問題
當對象中包含自身相互的引用時,,想要通過設(shè)置為NULL或者unset()來調(diào)用析構(gòu)函數(shù)可能會出現(xiàn)問題,。
class E
{
public $name;
public $obj;
public function __destruct()
{
echo "E:析構(gòu)函數(shù)被調(diào)用," . $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類的對象,,他們又各自持有對方的引用。其實簡單點來說的話,,自己持有自己的引用都會出現(xiàn)類似的問題,。
$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()那一行的注釋,析構(gòu)函數(shù)執(zhí)行的順序是這樣的:
// 不使用gc回收的結(jié)果
// E destory
// E:析構(gòu)函數(shù)被調(diào)用,e1
// -----
// E:析構(gòu)函數(shù)被調(diào)用,,e2
// -----
// E:析構(gòu)函數(shù)被調(diào)用,,e3
// -----
// E:析構(gòu)函數(shù)被調(diào)用,e4
// -----
如果我們打開了gc_collect_cycles()的注釋,,析構(gòu)函數(shù)的執(zhí)行順序是:
// 使用gc回收后結(jié)果
// E:析構(gòu)函數(shù)被調(diào)用,,e1
// -----
// E:析構(gòu)函數(shù)被調(diào)用,e2
// -----
// E destory
// E:析構(gòu)函數(shù)被調(diào)用,,e3
// -----
// E:析構(gòu)函數(shù)被調(diào)用,,e4
// -----
可以看出,必須要讓php使用gc回收一次,,確定對象的引用都被釋放了之后,,類的析構(gòu)函數(shù)才會被執(zhí)行。引用如果沒有釋放,,析構(gòu)函數(shù)是不會執(zhí)行的,。
構(gòu)造函數(shù)的低版本兼容問題
在PHP5以前,PHP的構(gòu)造函數(shù)是與類名同名的一個方法,。也就是說如果我有一個F類,,那么function F(){}方法就是它的構(gòu)造函數(shù)。為了向低版本兼容,,PHP依然保留了這個特性,,在PHP7以后如果有與類名同名的方法,就會報過時警告,,但不會影響程序執(zhí)行,。
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:這也是構(gòu)造函數(shù),與類同名,,不區(qū)分大小寫", 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:這也是構(gòu)造函數(shù),,與類同名", PHP_EOL;
// }
// function __construct(){
// echo "F:這是構(gòu)造函數(shù),__construct()", PHP_EOL;
// }
}
$f = new F();
如果__construc()和類同名方法同時存在的話,,會優(yōu)先走__construct(),。另外需要注意的是,函數(shù)名不區(qū)分大小寫,,所以F()和f()方法是一樣的都會成為構(gòu)造函數(shù),。同理,因為不區(qū)分大小寫,,所以f()和F()是不能同時存在的,。當然,我們都不建議使用類同名的函數(shù)來做為構(gòu)造函數(shù),,畢竟已經(jīng)是過時的特性了,,說不定哪天就被取消了,。
構(gòu)造函數(shù)重載
PHP是不運行方法的重載的,只支持重寫,,就是子類重寫父類方法,,但不能定義多個同名方法而參數(shù)不同。在Java等語言中,,重載方法非常方便,,特別是在類實例化時,可以方便地實現(xiàn)多態(tài)能力,。
$r1 = new R(); // 默認構(gòu)造函數(shù)
$r2 = new R('arg1'); // 默認構(gòu)造函數(shù) 一個參數(shù)的構(gòu)造函數(shù)重載,,arg1
$r3 = new R('arg1', 'arg2'); // 默認構(gòu)造函數(shù) 兩個參數(shù)的構(gòu)造函數(shù)重載,arg1,,arg2
就像上述代碼一樣,,如果你嘗試定義多個__construct(),PHP會很直接地告訴你運行不了,。那么有沒有別的方法實現(xiàn)上述代碼的功能呢,?當然有,否則咱也不會寫了,。
class R
{
private $a;
private $b;
public function __construct()
{
echo '默認構(gòu)造函數(shù)', 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 '一個參數(shù)的構(gòu)造函數(shù)重載,,' . $a, PHP_EOL;
$this->a = $a;
}
public function constructB($a, $b)
{
echo '兩個參數(shù)的構(gòu)造函數(shù)重載,' . $a . ',,' . $b, PHP_EOL;
$this->a = $a;
$this->b = $b;
}
}
$r1 = new R(); // 默認構(gòu)造函數(shù)
$r2 = new R('arg1'); // 默認構(gòu)造函數(shù) 一個參數(shù)的構(gòu)造函數(shù)重載,,arg1
$r3 = new R('arg1', 'arg2'); // 默認構(gòu)造函數(shù) 兩個參數(shù)的構(gòu)造函數(shù)重載,arg1,,arg2
相對來說比Java之類的語言要麻煩一些,但是也確實是實現(xiàn)了相同的功能哦,。
構(gòu)造函數(shù)和析構(gòu)函數(shù)的訪問限制
構(gòu)造函數(shù)和析構(gòu)函數(shù)默認都是public的,,和類中的其他方法默認值一樣。當然它們也可以設(shè)置成private和protected,。如果將構(gòu)造函數(shù)設(shè)置成非公共的,,那么你將無法實例化這個類。這一點在單例模式被廣泛應用,,下面我們直接通過一個單例模式的代碼看來,。
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想要實例化時,直接就報錯了,。關(guān)于單例模式為什么要讓外部無法實例化的問題,,我們可以看看之前的設(shè)計模式系統(tǒng)文章中的單例模式。
總結(jié)
沒想到我們天天用到的構(gòu)造函數(shù)還能玩出這么多花樣來吧,,日常在開發(fā)中比較需要注意的就是子類繼承時對構(gòu)造函數(shù)重寫時父類構(gòu)造函數(shù)的調(diào)用問題以及引用時的析構(gòu)問題,。
測試代碼:https://github.com/zhangyue0503/dev-blog/blob/master/php/201912/source/%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E4%BB%A5%E5%8F%8A%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0%E5%9C%A8PHP%E4%B8%AD%E9%9C%80%E8%A6%81%E6%B3%A8%E6%84%8F%E7%9A%84%E5%9C%B0%E6%96%B9.php
參考文檔:https://www./manual/zh/language.oop5.decon.php#105368https://www./manual/zh/language.oop5.decon.php#76446https://www./manual/zh/language.oop5.decon.php#81458https://www./manual/zh/language.oop5.decon.php