PHP Conference Japan 2024

建構子與解構子

建構子

__construct(mixed ...$values = ""): void

PHP 允許開發人員為類別宣告建構子方法。具有建構子方法的類別會在每次建立新物件時呼叫此方法,因此它適用於物件在使用前可能需要的任何初始化。

注意 如果子類別定義了建構子,則不會隱式呼叫父類別的建構子。為了執行父類別的建構子,需要在子建構子中呼叫 parent::__construct()。如果子類別沒有定義建構子,則可以像正常的類別方法一樣從父類別繼承(如果沒有宣告為 private)。

範例 #1 繼承中的建構子

<?php
class BaseClass {
function
__construct() {
print
"In BaseClass constructor\n";
}
}

class
SubClass extends BaseClass {
function
__construct() {
parent::__construct();
print
"In SubClass constructor\n";
}
}

class
OtherSubClass extends BaseClass {
// 繼承 BaseClass 的建構子
}

// In BaseClass constructor
$obj = new BaseClass();

// In BaseClass constructor
// In SubClass constructor
$obj = new SubClass();

// In BaseClass constructor
$obj = new OtherSubClass();
?>

與其他方法不同,__construct() 在擴充時不受通常的簽名相容性規則的約束。

建構子是在其對應物件的實例化期間呼叫的普通方法。因此,它們可以定義任意數量的引數,這些引數可以是必需的,可以具有型別,並且可以具有預設值。建構子引數的呼叫方式是在類別名稱後面的括號中放置引數。

範例 #2 使用建構子引數

<?php
class Point {
protected
int $x;
protected
int $y;

public function
__construct(int $x, int $y = 0) {
$this->x = $x;
$this->y = $y;
}
}

// 傳遞兩個參數。
$p1 = new Point(4, 5);
// 只傳遞必需的參數。$y 將採用其預設值 0。
$p2 = new Point(4);
// 使用具名參數(自 PHP 8.0 起):
$p3 = new Point(y: 5, x: 4);
?>

如果類別沒有建構子,或建構子沒有必需的引數,則可以省略括號。

舊式建構子

在 PHP 8.0.0 之前,全域命名空間中的類別會將與類別名稱相同的方法解釋為舊式建構子。該語法已棄用,並將導致 E_DEPRECATED 錯誤,但仍會將該函式呼叫為建構子。如果同時定義了 __construct() 和同名方法,則會呼叫 __construct()

在命名空間類別中,或自 PHP 8.0.0 起的任何類別中,與類別名稱相同的方法永遠不會有任何特殊意義。

在新程式碼中始終使用 __construct()

建構子升級

自 PHP 8.0.0 起,建構子參數也可以升級為對應於物件屬性。建構子參數通常在建構子中指派給屬性,但不會對其進行其他操作。建構子升級為該用例提供了一種簡寫方式。上面的範例可以重寫如下。

範例 #3 使用建構子屬性升級

<?php
class Point {
public function
__construct(protected int $x, protected int $y = 0) {
}
}

當建構子引數包含修飾詞時,PHP 會將其解釋為物件屬性和建構子引數,並將引數值指派給屬性。然後,建構子主體可以為空,也可以包含其他陳述式。在將引數值指派給對應的屬性後,將執行任何其他陳述式。

並非所有引數都需要升級。可以混合和匹配升級和未升級的引數,依任何順序。升級的引數對呼叫建構子的程式碼沒有影響。

注意:

使用可見性修飾詞publicprotectedprivate)是最有可能應用屬性升級的方式,但任何其他單一修飾詞(例如 readonly)都將具有相同的效果。

注意:

物件屬性由於引擎的模稜兩可性而無法輸入為 callable。因此,升級的引數也不能輸入為 callable。但是,允許任何其他型別宣告

注意:

由於升級的屬性會被解糖為屬性和函式參數,因此適用於屬性和參數的所有命名限制。

注意:

在提升的建構子引數上放置的屬性將會複製到屬性和引數。提升的建構子引數上的預設值只會複製到引數,而不會複製到屬性。

初始化器中的 new

自 PHP 8.1.0 起,物件可以用作預設參數值、靜態變數和全域常數,以及屬性引數。物件現在也可以傳遞給 define()

注意:

不允許使用動態或非字串類別名稱或匿名類別。不允許使用引數解包。不允許使用不支援的運算式作為引數。

範例 #4 在初始化器中使用 new

<?php

// 所有允許的:
static $x = new Foo;

const
C = new Foo;

function
test($param = new Foo) {}

#[
AnAttribute(new Foo)]
class
Test {
public function
__construct(
public
$prop = new Foo,
) {}
}

// 所有不允許的 (編譯時期錯誤):
function test(
$a = new (CLASS_NAME_CONSTANT)(), // 動態類別名稱
$b = new class {}, // 匿名類別
$c = new A(...[]), // 引數解包
$d = new B($abc), // 不支援的常數運算式
) {}
?>

靜態建立方法

PHP 每個類別只支援一個建構子。然而,在某些情況下,可能希望允許以不同的方式使用不同的輸入來建構物件。建議的方法是使用靜態方法作為建構子包裝器。

範例 #5 使用靜態建立方法

<?php
class Product {

private ?
int $id;
private ?
string $name;

private function
__construct(?int $id = null, ?string $name = null) {
$this->id = $id;
$this->name = $name;
}

public static function
fromBasicData(int $id, string $name): static {
$new = new static($id, $name);
return
$new;
}

public static function
fromJson(string $json): static {
$data = json_decode($json, true);
return new static(
$data['id'], $data['name']);
}

public static function
fromXml(string $xml): static {
// 此處為自訂邏輯。
$data = convert_xml_to_array($xml);
$new = new static();
$new->id = $data['id'];
$new->name = $data['name'];
return
$new;
}
}

$p1 = Product::fromBasicData(5, 'Widget');
$p2 = Product::fromJson($some_json_string);
$p3 = Product::fromXml($some_xml_string);

建構子可以設為 private 或 protected,以防止從外部呼叫。如果是這樣,只有靜態方法才能實例化該類別。因為它們位於相同的類別定義中,所以它們可以存取 private 方法,即使不是同一個物件實例。private 建構子是可選的,可能或可能沒有意義,具體取決於使用案例。

三個 public static 方法然後示範實例化物件的不同方式。

  • fromBasicData() 採用所需的确切參數,然後透過呼叫建構子並傳回結果來建立物件。
  • fromJson() 接受 JSON 字串,並自行對其進行一些預處理,以將其轉換為建構子所需的格式。然後傳回新的物件。
  • fromXml() 接受 XML 字串,對其進行預處理,然後建立一個裸物件。建構子仍會被呼叫,但由於所有參數都是可選的,因此方法會跳過它們。然後它會直接將值指派給物件屬性,然後傳回結果。

在這三個案例中,static 關鍵字都會被翻譯成程式碼所在的類別名稱。在本例中,為 Product

解構子

__destruct(): void

PHP 擁有類似於其他物件導向語言(如 C++)的解構子概念。只要不再有對特定物件的引用,或者在關機序列中的任何順序,就會呼叫解構子方法。

範例 #6 解構子範例

<?php

class MyDestructableClass
{
function
__construct() {
print
"In constructor\n";
}

function
__destruct() {
print
"Destroying " . __CLASS__ . "\n";
}
}

$obj = new MyDestructableClass();

與建構子一樣,引擎不會隱式呼叫父解構子。為了執行父解構子,必須在解構子主體中明確呼叫 parent::__destruct()。同樣與建構子一樣,如果子類別本身沒有實作解構子,則可以繼承父解構子。

即使使用 exit() 停止腳本執行,也會呼叫解構子。在解構子中呼叫 exit() 將會阻止執行剩餘的關機常式。

如果解構子建立對其物件的新引用,則當引用計數再次達到零或在關機序列期間,將不會再次呼叫它。

自 PHP 8.4.0 起,當 循環收集在執行 Fiber 期間發生時,排程用於收集的物件的解構子會在單獨的 Fiber 中執行,稱為 gc_destructor_fiber。如果此 Fiber 被暫停,將會建立一個新的 Fiber 來執行任何剩餘的解構子。之前的 gc_destructor_fiber 將不再被垃圾收集器引用,如果沒有在其他地方引用,可能會被收集。解構子被暫停的物件將不會被收集,直到解構子傳回或 Fiber 本身被收集。

注意:

在腳本關機期間呼叫的解構子已經發送了 HTTP 標頭。在某些 SAPI(例如 Apache)中,腳本關機階段中的工作目錄可能會有所不同。

注意:

嘗試從解構函式(在腳本終止時呼叫)拋出例外會導致嚴重錯誤。

新增註解

使用者提供的註解 14 則註解

david dot scourfield at llynfi dot co dot uk
13 年前
請注意物件內的循環參考可能導致的記憶體洩漏。PHP 手冊指出「[解構函式]方法將在移除對特定物件的所有參考時立即被呼叫」,這確實是正確的:如果兩個物件互相參考(或者甚至一個物件有一個指向自身的欄位,如 $this->foo = $this),那麼即使完全沒有其他對該物件的參考,這個參考也會阻止解構函式被呼叫。程式設計師將無法再存取這些物件,但它們仍然會留在記憶體中。

請考慮以下範例

<?php

header
("Content-type: text/plain");

class
Foo {

/**
* 一個識別碼
* @var string
*/
private $name;
/**
* 對另一個 Foo 物件的參考
* @var Foo
*/
private $link;

public function
__construct($name) {
$this->name = $name;
}

public function
setLink(Foo $link){
$this->link = $link;
}

public function
__destruct() {
echo
'正在銷毀:', $this->name, PHP_EOL;
}
}

// 建立兩個 Foo 物件:
$foo = new Foo('Foo 1');
$bar = new Foo('Foo 2');

// 讓它們互相指向對方
$foo->setLink($bar);
$bar->setLink($foo);

// 銷毀對它們的全域參考
$foo = null;
$bar = null;

// 我們現在無法存取 Foo 1 或 Foo 2,所以它們應該被 __destruct(),
// 但它們沒有,所以我們產生了記憶體洩漏,因為它們仍然在記憶體中。
//
// 取消註解下一行以查看明確呼叫 GC 時的差異:
// gc_collect_cycles();
//
// 另請參閱: https://php.dev.org.tw/manual/en/features.gc.php
//

// 建立另外兩個 Foo 物件,但不要設定它們的內部 Foo 參考
// 所以除了變數 $foo 和 $bar 之外,沒有任何東西指向它們:
$foo = new Foo('Foo 3');
$bar = new Foo('Foo 4');

// 銷毀對它們的全域參考
$foo = null;
$bar = null;

// 我們現在無法存取 Foo 3 或 Foo 4,並且由於沒有其他地方對它們的參考,它們的 __destruct() 方法會在
// 這裡自動被呼叫,在執行下一行之前:

echo '腳本結束', PHP_EOL;

?>

這會輸出

正在銷毀:Foo 3
正在銷毀:Foo 4
腳本結束
正在銷毀:Foo 1
正在銷毀:Foo 2

但是,如果我們在腳本中間取消註解 gc_collect_cycles(); 函數呼叫,我們會得到

正在銷毀:Foo 2
正在銷毀:Foo 1
正在銷毀:Foo 3
正在銷毀:Foo 4
腳本結束

如所期望的。

注意:呼叫 gc_collect_cycles() 會產生速度上的開銷,因此只有在您認為需要時才使用它。
Hayley Watson
1 年前
使用靜態工廠方法來封裝物件建構而不是直接的建構函式呼叫還有其他優點。

除了允許在不同場景中使用不同的方法,這些方法和參數都有更相關的名稱,並且建構函式不必處理不同類型的不同參數集之外

* 您可以在嘗試建構物件之前完成所有輸入驗證。
* 物件本身可以在建構自身類別的新實例時繞過該輸入驗證,因為您可以確保它知道自己在做什麼。
* 由於輸入驗證/預處理已移至工廠方法,因此建構函式本身通常可以簡化為「將這些屬性設定為這些參數」,這意味著建構函式提升語法變得更有用。
* 由於已對使用者隱藏,因此建構函式的簽名可以稍微醜陋一些,而不會給他們帶來麻煩。呵呵。
* 靜態方法可以被提取並作為一級閉包傳遞,以便在可以呼叫函數的任何地方以正常方式呼叫,而無需特殊的「new」語法。
* 工廠方法不必傳回該確切類別的新實例。它可以傳回與新實例執行相同工作的預先存在的實例(尤其在透過減少重複項目的不可變「值類型」物件的情況下非常有用);或者是一個更簡單或更特定的子類,以比原始類別的更通用實例更少的開銷來完成工作。傳回子類表示 LSP 仍然成立。
domger at freenet dot de
7 年前
__destruct 魔術方法必須是 public。

public function __destruct()
{
;
}

該方法將在實例外部自動被呼叫。將 __destruct 宣告為 protected 或 private 將導致警告,並且不會呼叫魔術方法。

注意:在 PHP 5.3.10 中,我看到一些解構函式宣告為 protected 時出現了奇怪的副作用。
spleen
16 年前
總是那些簡單的事情會讓你卡住 -

身為 OOP 新手,我花了很長的時間才弄清楚 __construct 這個詞前面有兩個底線。

它是 __construct
而不是 _construct

一旦弄清楚就非常明顯,但在你弄清楚之前會非常令人沮喪。

我花了很多不必要的時間來除錯正在執行的程式碼。

我甚至想過幾次,認為範例中看起來有點長,但當時這似乎很愚蠢(總認為「哦,如果它不是普通的底線,有人會把它弄清楚...」)

我瀏覽過的所有手冊、閱讀的所有教程、瀏覽過的所有範例 - 沒有人提到這一點!

(請不要告訴我它在這個頁面的某個地方有解釋,我只是錯過了,你只會增加我的痛苦。)

我希望這能幫助其他人!
iwwp at outlook dot com
4 年前
為了更好地理解 __destruct 方法

class A {
protected $id;

public function __construct($id)
{
$this->id = $id;
echo "construct {$this->id}\n";
}

public function __destruct()
{
echo "destruct {$this->id}\n";
}
}

$a = new A(1);
echo "-------------\n";
$aa = new A(2);
echo "=============\n";

輸出內容

construct 1
-------------
construct 2
=============
destruct 2
destruct 1
mmulej at gmail dot com
2 年前
*<重複發文>我無法編輯我先前的註解來闡述修飾詞。請原諒我。*

如果父類別和子類別都有定義名稱相同的方法,並且在父類別的建構函式中呼叫它,則使用 `parent::__construct()` 將會呼叫子類別中的方法。

<?php

class A {
public function
__construct() {
$this->method();
}
public function
method() {
echo
'A' . PHP_EOL;
}
}
class
B extends A {
public function
__construct() {
parent::__construct();
}
}
class
C extends A {
public function
__construct() {
parent::__construct();
}
public function
method() {
echo
'C' . PHP_EOL;
}
}
$b = new B; // A
$c = new C; // C

?>

在這個範例中,A::method 和 C::method 都是 public。

您可以將 A::method 變更為 protected,並將 C::method 變更為 protected 或 public,它仍然會以相同的方式運作。

但是,如果您將 A::method 設定為 private,則無論 C::method 是 private、protected 還是 public 都沒有關係。$b 和 $c 都將輸出 'A'。
david at synatree dot com
16 年前
當腳本正在執行 die() 時,你無法確定 __destruct() 的呼叫順序。

在我正在開發的一個腳本中,我希望對所有傳出的資料進行透明的底層加密。為了實現這一點,我使用了一個全域單例類別,其配置如下:

class EncryptedComms
{
private $C;
private $objs = array();
private static $_me;

public static function destroyAfter(&$obj)
{
self::getInstance()->objs[] =& $obj;
/*
希望藉由強制一個對其他物件的引用存在
在這個類別內,被引用的物件需要在這個物件進行垃圾回收之前被銷毀。
這會強制
這個物件的解構方法在所有這裡引用的物件的解構方法執行「之後」才被觸發。
所有在這裡引用的物件的解構方法執行「之後」才被觸發。
*/
}
public function __construct($key)
{
$this->C = new SimpleCrypt($key);
ob_start(array($this,'getBuffer'));
}
public static function &getInstance($key=NULL)
{
if(!self::$_me && $key)
self::$_me = new EncryptedComms($key);
else
return self::$_me;
}

public function __destruct()
{
ob_end_flush();
}

public function getBuffer($str)
{
return $this->C->encrypt($str);
}

}

在這個例子中,我試圖註冊其他物件,讓它們總是在這個物件之前被銷毀。像這樣:

class A
{

public function __construct()
{
EncryptedComms::destroyAfter($this);
}
}

人們會認為單例中包含的物件的引用會先被銷毀,但事實並非如此。事實上,即使你反轉這種模式,並在每個你希望在其之前被銷毀的物件中儲存一個對 EncryptedComms 的引用,它也不會起作用。

簡而言之,當腳本執行 die() 時,似乎沒有任何方法可以預測解構函式的觸發順序。
prieler at abm dot at
17 年前
我寫了一個關於 php 5.2.1 中解構函式和關閉函式順序的簡單範例。

<?php
class destruction {
var
$name;

function
destruction($name) {
$this->name = $name;
register_shutdown_function(array(&$this, "shutdown"));
}

function
shutdown() {
echo
'shutdown: '.$this->name."\n";
}

function
__destruct() {
echo
'destruct: '.$this->name."\n";
}
}

$a = new destruction('a: global 1');

function
test() {
$b = new destruction('b: func 1');
$c = new destruction('c: func 2');
}
test();

$d = new destruction('d: global 2');

?>

這會輸出
shutdown: a: global 1
shutdown: b: func 1
shutdown: c: func 2
shutdown: d: global 2
destruct: b: func 1
destruct: c: func 2
destruct: d: global 2
destruct: a: global 1

結論
解構函式總是在腳本結束時被呼叫。
解構函式依照它們的「上下文」順序被呼叫:先是函數內的,然後是全域物件。
函數上下文中的物件依照它們被設定的順序被刪除(較舊的物件優先)。
全域上下文中的物件依照相反的順序被刪除(較舊的物件最後)。

關閉函式在解構函式之前被呼叫。
關閉函式依照它們的「註冊」順序被呼叫。;)

問候,J
Per Persson
12 年前
從 PHP 5.3.10 開始,由於致命錯誤而導致的關閉不會執行解構函式。

例如
<?php
class Logger
{
protected
$rows = array();

public function
__destruct()
{
$this->save();
}

public function
log($row)
{
$this->rows[] = $row;
}

public function
save()
{
echo
'<ul>';
foreach (
$this->rows as $row)
{
echo
'<li>', $row, '</li>';
}
echo
'</ul>';
}
}

$logger = new Logger;
$logger->log('Before');

$nonset->foo();

$logger->log('After');
?>

如果沒有 $nonset->foo(); 這行,Before 和 After 都會被列印,但如果有這行,則兩者都不會被列印。

但是,可以將解構函式或另一個方法註冊為關閉函式。
<?php
class Logger
{
protected
$rows = array();

public function
__construct()
{
register_shutdown_function(array($this, '__destruct'));
}

public function
__destruct()
{
$this->save();
}

public function
log($row)
{
$this->rows[] = $row;
}

public function
save()
{
echo
'<ul>';
foreach (
$this->rows as $row)
{
echo
'<li>', $row, '</li>';
}
echo
'</ul>';
}
}

$logger = new Logger;
$logger->log('Before');

$nonset->foo();

$logger->log('After');
?>
現在 Before 會被列印,但 After 不會,因此你可以看到在 Before 之後發生了關閉。
bolshun at mail dot ru
16 年前
確保某個類別的實例在另一個類別的解構函式中可用很容易:只需在這個另一個類別中保留對該實例的引用即可。
Yousef Ismaeil cliprz[At]gmail[Dot]com
11 年前
<?php

/**
* 一個有趣的 Mobile 類別範例
*
* @author Yousef Ismaeil Cliprz[At]gmail[Dot]com
*/

class Mobile {

/**
* 一些裝置屬性
*
* @var string
* @access public
*/
public $deviceName,$deviceVersion,$deviceColor;

/**
* 設定 Mobile::屬性 的值
*
* @param string 裝置名稱
* @param string 裝置版本
* @param string 裝置顏色
*/
public function __construct ($name,$version,$color) {
$this->deviceName = $name;
$this->deviceVersion = $version;
$this->deviceColor = $color;
echo
"The ".__CLASS__." 類別已啟動。<br /><br />";
}

/**
* 一些輸出
*
* @access public
*/
public function printOut () {
echo
'我有一支 '.$this->deviceName
.' 版本 '.$this->deviceVersion
.' 我的裝置顏色是 : '.$this->deviceColor;
}

/**
* 嗯,只是為了範例,我們要移除 Mobile::$deviceName,嗯,不是 unset,只是為了檢查 __destruct 如何運作
*
* @access public
*/
public function __destruct () {
$this->deviceName = '已移除';
echo
'<br /><br />正在傾印 Mobile::deviceName 以確保它已移除,好的:';
var_dump($this->deviceName);
echo
"<br />The ".__CLASS__." 類別已關閉。";
}

}

// 喔耶,建立實例
$mob = new Mobile('iPhone','5','黑色');

// 印出輸出
$mob->printOut();

?>

Mobile 類別已啟動。

我有一支 iPhone 版本 5 我的裝置顏色是 : 黑色

正在傾印 Mobile::deviceName 以確保它已移除,好的
string '已移除' (length=7)

The Mobile 類別已關閉。
Jonathon Hibbard
14 年前
請注意在使用 __destruct() 時,您正在取消設定變數...

請考慮以下程式碼
<?php
class my_class {
public
$error_reporting = false;

function
__construct($error_reporting = false) {
$this->error_reporting = $error_reporting;
}

function
__destruct() {
if(
$this->error_reporting === true) $this->show_report();
unset(
$this->error_reporting);
}
?>

以上程式碼將會導致錯誤
Notice: Undefined property: my_class::$error_reporting in my_class.php on line 10

似乎在 if 敘述實際執行之前,變數就會被取消設定。移除 unset 將會修復此問題。反正 PHP 也會釋放所有東西,所以不需要這樣做,但為了以防萬一您遇到此情況,您就會知道原因 ;)
Reza Mahjourian
18 年前
Peter 建議使用靜態方法來彌補 PHP 中無法使用多個建構子的情況。這在大多數情況下都運作良好,但如果您有一個類別階層結構,並且想要將部分初始化委派給父類別,您就不能再使用這個方法了。這是因為與建構子不同,在靜態方法中,您需要自行執行實例化。因此,如果您呼叫父靜態方法,您將獲得父類別類型的物件,而您無法繼續使用衍生類別欄位來初始化它。

想像一下,您有一個 Employee 類別和一個衍生 HourlyEmployee 類別,並且您也想能夠從某些 XML 輸入中建構這些物件。

<?php
class Employee {
public function
__construct($inName) {
$this->name = $inName;
}

public static function
constructFromDom($inDom)
{
$name = $inDom->name;
return new
Employee($name);
}

private
$name;
}

class
HourlyEmployee extends Employee {
public function
__construct($inName, $inHourlyRate) {
parent::__construct($inName);
$this->hourlyRate = $inHourlyRate;
}

public static function
constructFromDom($inDom)
{
// 無法呼叫 parent::constructFromDom($inDom)
// 需要再次在這裡完成所有工作
$name = $inDom->name; // 增加耦合
$hourlyRate = $inDom->hourlyrate;
return new
EmployeeHourly($name, $hourlyRate);
}

private
$hourlyRate;
}
?>

唯一的解決方案是將兩個建構子合併為一個,方法是向每個建構子新增一個可選的 $inDom 參數。
ziggy at start dot dust
2 年前
請注意,建構子引數提升有點半生不熟(至少在 8.1 版中是這樣,而且看起來在 8.2 版中也沒有改變),而且您不允許將提升的引數與其他提升的引數重複使用。例如,有一個「舊式」建構子

<?php
public function __construct(protected string $val, protected Foo $foo = null) {
$this->val = $val;
$this->foo = $foo ?? new Foo($val);
}
?>

您將無法像這樣使用引數提升

<?php
public function __construct(protected string $val, protected Foo $foo = new Foo($val)) {}
?>

或者

<?php
public function __construct(protected string $val, protected Foo $foo = new Foo($this->val)) {}
?>

因為在這兩種情況下,您都會遇到「PHP 致命錯誤:常數運算式包含無效的運算」。
To Top