2017年8月10日 星期四

PHP 參照 (References)

參照 (References) 在 PHP 裡指的是使用不同的變數名稱 (Variable Names) 來存取相同的變數內容 (Variable Content),是符號表 (Symbol Table) 上的別名 (Aliases)。PHP 本身並沒有指標 (Pointer) 這種資料類型,跟 Java 裡的參照也不同;PHP 的參照無法直接執行記憶體位址運算,也不是實際的記憶體位址。PHP 使用參照的目的為減少複製變數時的記憶體耗用以及增加程式的效能,特別是在複製陣列、字串時。

基本運算

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.


沒有留言: