基本運算
PHP 的參照包含三種基本運算:指派參照、傳參照、返回參照等。指派參照 (Assign by Reference)
在 PHP 中變數要參照到其他的變數內容使用的是 =& 這個參照指派 (賦值) 運算子:<?php $a = 10; $b =& $a; $b = 20; echo $a . PHP_EOL; // 20 $a = 15; echo $b . PHP_EOL; // 15
在上例中透過 $b =& $a;,$a 和 $b 指向了同一個變數內容,$a 與 $b 互為別名變數,因此不論是重新指定 $a 的值或是 $b 的值,都會改變此一變數內容。
當對變數再次使用 =& 運算子指派參照時,變數會重新指向新的變數內容;而原有的其他別名變數則是不會受到影響,還是指向原來的變數內容。
// 承上例 $c = 30; $b =& $c; $b = 40; echo $a . PHP_EOL; // 15 echo $b . PHP_EOL; // 40 echo $c . PHP_EOL; // 40
$b 被重新指派到新的變數內容,並不會影響 $a 原來指向的變數內容。
當使用 unset() 取消設置變數時,其他的別名變數並不會受到影響。若是變數內容都沒有變數指向它時,則 PHP 的垃圾收集 (Garbage Collection) 機制會回收變數內容的記憶體空間。
// 承上例 unset($c); echo $b . PHP_EOL; // 40
別名變數還是一般變數,透過指派運算子 = 將別名變數指派給其他的變數時,會是指派值而不是指派參照。
// 承上例 $d = $b; $b = 5; echo $d . PHP_EOL; // 40
PHP 函式中的 global $var; 宣告是 $var =& $GLOBALS['var']; 的快捷設定,實際上是讓 $var 這個區域變數參照到外部 $var 的變數內容。當 $var 被指派為另一個變數內容的參照時,就不再是外部 $var 的別名變數,此時在函式內變動 $var 的值,就只會在函式內有作用。
<?php $bar = 10; $baz = 15; $qux = 30; function foo() { global $bar, $baz, $qux; $quux = 45; $bar =& $quux; $baz =& $qux; $baz = 100; } foo(); echo $bar . PHP_EOL; // 10 echo $baz . PHP_EOL; // 15 echo $qux . PHP_EOL; // 100
在上例中 $bar、$baz、$qux 一開始分別參照到相對應外部變數的變數內容,但是在 $bar =& $quxx; 以及 $baz =& $qux; 陳述句中,$bar (與 $quux 成為別名變數) 以及 $baz (與 $qux 成為別名變數) 不再參照到外部的變數內容,而使得外部變數內容不再受到它們在函式內重新指派值的影響。至於 $qux 仍然是外部變數的別名變數,改變它的另一個別名變數 $baz 的值,還是會影響到外部變數內容。
如果要在函式內同時使用 global 與指派參照時,建議使用 $GLOBALS 陣列,減少因為使用快捷設定所可能造成的混淆。
要讓變數和陣列元素變數指向相同的變數內容,可以在指派變數為陣列元素時,在變數前面加上 & 前綴;也可以將陣列元素變數以指派參照的方式,指派給變數。參照在陣列中會具有一些風險:在一般指派 (沒有使用 =& 運算子) 時,如果指派運算子右側陣列變數的元素中有參照,雖然不會讓左側陣列變數成為參照;但是陣列中的參照卻會保留。
<?php $a = 1; $arr = array(&$a, 3, 5); $b =& $arr[1]; //$a and $arr[0] are in the same reference set $arr2 = $arr; //not an assignment-by-reference! $arr2[0]++; $arr2[1]++; $arr2[2]++; echo $a . PHP_EOL; // 2 echo $b . PHP_EOL; // 4 print_r($arr); // $arr == array(2, 4, 5)
在上例中,$arr[0] 和 $a 指向相同的變數內容,$arr[1] 和 $b 互為別名變數。在將 $arr 一般指派給 $arr2 後,$arr2[0]、$arr[0] 和 $a 互為別名變數,$arr2[1]、$arr[1] 和 $b 指向相同的變數內容;但是 $arr2 不會是 $arr1 的別名變數。
從 PHP 5 開始,建立物件的 new 運算子會自動返回參照。就技術上來說,自動返回的是類似於資源 (Resource) 變數用的識別碼 (Identifier);是一個指向實際物件資料的指標,而不是上述的別名。所以在使用 new 運算子時,不需要使用 =&。而從 PHP 7.0 以後,語法將不再支援 new 建立物件時使用 =&。有關物件與參照,請參考本文下面的說明。
傳參照 (Pass by Reference)
PHP 在呼叫函式時預設是採用傳值 (Pass by Value) 的方式:當傳遞給函式的參 (引) 數是變數時,會將來源變數的複本 (而不是來源變數本身) 傳入函數中進行運算;在函式內改變的是複本變數的值,而不是來源變數的值。如果函式想要變更來源變數的值,那就可以採用傳參照的方式。傳參照是將來源變數的參照傳入函式中進行運算 (與參數名稱對映的變數成為來源變數的別名變數),而不是在函式中建立一個複本變數。此時如果在函式中改變了別名變數的值,來源變數的值也會同時改變。當決定函式的參數要採用傳參照的方式時,函式定義需要在參數名稱前面加上 & 前綴。呼叫函式的方式則是跟傳值時是一樣的。例如,當我們要交換兩個變數的值時,可以使用下列的程式:
<?php function Swap(&$a, &$b) { $temp = $a; $a = $b; $b = $temp; } $var1 = 10; $var2 = 20; Swap($var1, $var2); echo $var1 . PHP_EOL; //20 echo $var2 . PHP_EOL; //10
Swap() 函式用來交換兩個變數的值,在定義時兩個參數前面皆加上了上 & 符號;在呼叫函式時,只要直接將要交換的變數傳入即可。如果函式定義時沒有在參數前加上 & 符號,那麼傳入的兩個變數的值將不會被交換。
<?php function Swap($a, $b) { $temp = $a; $a = $b; $b = $temp; } $var1 = 10; $var2 = 20; Swap($var1, $var2); echo $var1 . PHP_EOL; //10 echo $var2 . PHP_EOL; //20
當然,也可以在函式內使用 global 或是使用 $GLOBALS 陣列讓區域變數和外部變數互為別名變數。只是在呼叫函式時永遠都只會交換參照變數,讓函式的使用受到了限制。
<?php function Swap() { global $var1, $var2; $temp = $var1; $var1 = $var2; $var2 = $temp; } $var1 = 10; $var2 = 20; Swap(); echo $var1 . PHP_EOL; //20 echo $var2 . PHP_EOL; //10
下面的範例則是使用了動能變數 (Variable Variables)來解決上例中只能夠交換參照變數的問題。(這個範例雖然同樣可以達到交換變數的結果,但是較為複雜,不建議使用。)
<?php function Swap($a, $b) { $x =& $GLOBALS[$a]; $y =& $GLOBALS[$b]; $temp = $x; $x = $y; $y = $temp; } $var1 = 'u'; $$var1 = 10; $var2 = 'v'; $$var2 = 20; Swap($var1, $var2); echo $$var1 . PHP_EOL; //20 echo $$var2 . PHP_EOL; //10
在同時使用指派參照和傳參照時要特別小心,下面是一個錯誤的使用範例 (想要參照函式所選定的變數)。在呼叫函式時 $var 會跟 $bar 綁定,可是隨後會跟 $GLOBALS["baz"] 重新綁定;在 PHP 的參照機制下,造成無法綁定 $bar。
<?php $baz = 15; function foo(&$var) { $var =& $GLOBALS["baz"]; } foo($bar); var_dump($bar); // NULL
想要參照函式所選定的變數,我們可以使用下一節介紹的返回參照。
傳參照的好處
PHP 的變數基本上是使用 ZVAL 這個結構所產生,在指派值、傳遞參數、返回值的時候都需要使用這個結構。在 PHP 7 對 ZVAL 有了大幅度的變動 ,讓 PHP 在記憶體使用以及效能有了很大的進步。雖然如此,還是會有一些類型的變數 (例如,陣列、字串、物件等) 會需要較大記憶體空間。如果是以單純指派值、傳值的方式會讓記憶體空的使用倍增,例如,當要把一個大的陣列內的值都加以平方時,傳值的方式需要使用兩倍的記憶體空間:<?php function memoryUseNow() { $level = array('Bytes', 'KB', 'MB', 'GB'); $n = memory_get_usage(); for ($i=0, $max=count($level); $i<$max; $i++){ if ($n < 1024){ $n = round($n, 2); return "{$n} {$level[$i]}"; } $n /= 1024; } } function squareArray($array) { $square = []; foreach($array as $key => $val) { $square[$key] = $val * $val; } return $square; } echo memoryUseNow() . PHP_EOL; // 348.69 KB for($i = 0; $i <400000; $i++) $b[] = 8; echo memoryUseNow() . PHP_EOL; // 18.34 MB $c = squareArray($b); echo memoryUseNow() . PHP_EOL; // 36.34 MB
可是如果改為傳參照,則可以減少記憶體的使用量:
<?php function memoryUseNow() { $level = array('Bytes', 'KB', 'MB', 'GB'); $n = memory_get_usage(); for ($i=0, $max=count($level); $i<$max; $i++){ if ($n < 1024){ $n = round($n, 2); return "{$n} {$level[$i]}"; } $n /= 1024; } } function squareArrayRef(&$array) { foreach($array as $key => $val) { $array[$key] = $val * $val; } } echo memoryUseNow() . PHP_EOL; // 348.35 KB for($i = 0; $i <400000; $i++) $b[] = 8; echo memoryUseNow() . PHP_EOL; // 18.34 MB squareArrayRef($b); echo memoryUseNow() . PHP_EOL; // 18.34 MB
返回參照 (Return by Reference)
返回參照可以讓函式 (方法) 找到參照應該被綁定在那一個變數 (屬性) 上,可以應用在物件導向程式設計或是閉包 (Closure) 中。和傳參照不同,返回參照需要同時在函式定義和呼叫函式時使用 &:在函式名稱前加入 & 前綴代表返回的是一個參照而不是一個複本;在呼叫函式時使用參照指派運算子 =& 表示變數要參照到變數內容,而不是一般的指派值。下面的例子示範了參照返回的語法與用途:<?php class foo { private $value = 80; public function &getValue() { return $this->value; } public function setValue($input) { $this->value = $input; } } $obj = new foo; $myRefValue = &$obj->getValue(); // $myValue is a reference to $obj->value $myAssValue = $obj->getValue(); // 80 is assigned to $myAssValue echo $myRefValue . PHP_EOL; // 80 echo $myAssValue . PHP_EOL; // 80 $obj->setValue(20); echo $myRefValue . PHP_EOL; // 20 echo $myAssValue . PHP_EOL; // 80 $myRefValue = 30; echo $obj->getValue() . PHP_EOL; //30
在上例的類別中,私有變數 (Private Variable) $value 可以透過取得方法 (Get Method) getValue() 以及設定方法 (Set Method) setValue() 進行變數存取設定;其中 getValue() 會視變數在賦值時所使用的指派運算子類型來決定是純粹的指派值 ($myAssValue),還是返回參照 ($myRefValue)。當 getValue() 是以返回參照的方式使用時,私有變數會被參照到外部變數,變數內容可以藉由改變別名變數 (外部變數) 的值來變更。雖然可以方便設定私有變數,但是也一定程度違反了物件導向的封裝 (Encapsulation) 精神。
PHP 官網手冊的語言索引的返回參照一節中有一個注意事項:參照返回時的返回值必需是變數,不能夠是表達示 (Expression),否則在 PHP 5.1.0 以後會發出 E_NOTICE 錯誤,並其舉例說明 return ($this->value); 會無法運作。但是,這個注意事項是的對,舉例則是不恰當。因為在 PHP 7.1 return ($this->value); 是可以運作的 (即在 PHP 7.1 return ($this->value); 返回的還是變數),不過如果是 return ($this->value + 1); 則一定會出現錯誤。
在使用參照返回時要注意,不應該因為要增進效能而使用參照返回;因為 Zend Engine 有自動最佳化效能的方法。使用者應該只有在存在有效的技術理由時使用參照返回。
物件與參照 (Objects and references)
自從 PHP 5 開始,物件變數的變數內容不再包含物件本身;它僅僅包含一個能夠讓物件的存取者找到實際物件的物件識別碼 (Object Identifier),是一個指向實際物件資料的指標。當一個物件被當作參數傳送、返回到或是指派到另一個變數時,這些變數不是物件的複本也不是別名變數,而是各自保有指向相同物件的識別碼的複本。因此,「物件預設是傳參照」這個在 PHP 物件導向程式設計時,經常被提出的重點,並不是完全正確。<?php class Foo { private static $used = 0; private $id; public function __construct() { $this->id = self::$used++; } public function __destruct() { echo $this->id . ' destroyed.' . PHP_EOL; } public function __clone() { $this->id = self::$used++; } public function getId() { return $this->id; } } $a = new Foo; $b = $a; // $a and $b are copies of the same identifier // ($a) = ($b) = <id0> echo $a->getId() . PHP_EOL; // 0 echo $b->getId() . PHP_EOL; // 0 $c =& $a; // $a and $c are references // ($a, $c) = ($b) = <id0> echo $c->getId() . PHP_EOL; // 0 $a = new Foo; // ($a, $c) = <id1>, ($b) = <id0> echo $a->getId() . PHP_EOL; // 1 echo $b->getId() . PHP_EOL; // 0 echo $c->getId() . PHP_EOL; // 1 unset($a); // ($b) = <id0>, ($c) = <id1>, $a =& $b; // ($a, $b) = <id0>, ($c) = <id1> echo $a->getId() . PHP_EOL; // 0 echo $b->getId() . PHP_EOL; // 0 echo $c->getId() . PHP_EOL; // 1 $a = NULL; // $a and $b now become a reference to NULL. // 0 destroyed. unset($c); // 1 destroyed. echo "Completed."; // Completed.
在上例中,可以看到對於物件變數使用指派運算子和參照指派運算子的不同:1) 當兩個物件變數互為參照時 (使用 =& 時),將其中的一個變數指向另一個的實際物件資料時 (例如使用 new 建立新物件,或是使用 = 建立不同的物件識別碼複本時),另一個變數也會同時指向該實際物件資料; 2) 當兩個物件變數是相同物件識別碼的複本時 (使用 = 時),將其中的一個變數指向另一個實際物件資料時,只會讓指向原來實際物件資料的計數減 1 (當指向實際物件資料的計數為 0 時,垃圾收集機制就會回收實際物件資料的記憶體空間,__destruct() 會自動被呼叫),而不會改變另一個變數的物件識別碼。__construct()、__destruct()這二個方法是 PHP 提供的魔術方法 (Magic Method),使用者無法直接調用 (invoke) 它;分別只能夠在建立物件時、解構物件時時調用。
物件複製 (Object Cloning)
因為物件無法像純量類型 (Scalar Types) 一樣使用 = 指派運算子進複製,因此需要使用關鍵字 clone 來複製物件。例如:$bar = clone $foo;
會建立物件 $foo 的複本 $bar,$bar 跟 $foo 是屬於同一個類別、具有相同屬性值的獨立物件。要注意的是 clone 是一種淺複製 (Shallow Copy):任何參照到其他變數的屬性都將保留參照;而含有物件識別碼的屬性都會建立物件識別碼複本。
<?php class Person { private static $used = 0; private $id; private $gid; private $name; public function __construct($name) { $this->name = $name; $this->id = self::$used++; } public function __destruct() { echo $this->id . ' destroyed.' . PHP_EOL; } public function setName($name) { $this->name = $name; } public function getName() { return $this->name; } public function setGid(&$id) { $this->gid = $id; } public function getGid() { return $this->gid; } public function getId() { return $this->id; } } class WorkingGroup { private $leader; public function setLeader($leader) { $this->leader = $leader; } public function getLeader() { return $this->leader; } } $jeff = new Person("Jeff Lamb"); $wg1 = new WorkingGroup(); $wg1->setLeader($jeff); $wg2 = clone $wg1; $gid = 100; $wg2->getLeader()->setGid($gid); echo $wg1->getLeader()->getGid() . PHP_EOL; // 100 echo $wg2->getLeader()->getGid() . PHP_EOL; // 100 echo $wg1->getLeader()->getId() . PHP_EOL; // 0 echo $wg2->getLeader()->getId() . PHP_EOL; // 0 $jeff = null; $wg1->setLeader(null); $wg2->setLeader(null); // 0 destroyed. echo "Completed."; // Completed.
建立一個完全複製所有屬性的物件複本,有時候並不是我們想要的結果,特別是參照和物件識別碼都被保留、複製時。例如,在上例中每個工作小組 (Working Group) 都會有自己的小組領導人,此時使用淺複製會讓兩個物件的負責人都是同一個。需要重新指派 $wg2 的小組領導人物件,才可以讓兩個工作群組的領導人不再是同一個:
$mary = new Person("Mary Jones"); $wg2->setLeader($mary);
如果要改變預設的物件複製行為,我們需要在上例的基底類別 WorkingGroup 中建立一個名稱為 __clone() 的魔術方法。跟建構式以及解構式 一樣,這個方法使用者無法直接調用它;只有在使用 clone 複製物件時,才會被調用。在 __clone() 中可以定義想要的複製行為,它會在預設的複製行為完作後被呼叫。 __clone() 最常被用來確保作為參照處理的屬性能夠被正確複製。當要複製的物件中包含有指向其他實際物件資料的屬性時,此時希望能夠得到的是該實際物件資料的副本,而不是物件識別碼的複本。
<?php class Person { . . . public function __clone() { $this->id = self::$used++; } . . . } class WorkingGroup { private $leader; public function __clone() { $this->leader = clone $this->leader; } . . . } $jeff = new Person("Jeff Lamb"); $wg1 = new WorkingGroup(); $wg1->setLeader($jeff); $wg2 = clone $wg1; $gid = 100; $wg2->getLeader()->setGid($gid); echo $wg1->getLeader()->getGid() . PHP_EOL; // echo $wg2->getLeader()->getGid() . PHP_EOL; // 100 echo $wg1->getLeader()->getId() . PHP_EOL; // 0 echo $wg2->getLeader()->getId() . PHP_EOL; // 1 $jeff = null; $wg1->setLeader(null); // 0 destroyed. $wg2->setLeader(null); // 1 destroyed. echo "Completed."; // Completed.
沒有留言:
張貼留言