PHP 引用是個坑,請慎用

file

去年我參加了不少次會議,其中八次會議裏我進行了相關發言,這其中我屢次談到了 PHP 的引用問題,由於不少人對它的理解有所誤差。在深刻討論這個問題以前,咱們先回顧一下引用的基本概念,明確什麼是「引用傳遞」。php

在 PHP 中引用意味着用不一樣的名字訪問同一個變量內容,不論你用哪一個名字對變量作出了運算,其餘名字訪問的內容也將改變。laravel

讓咱們經過代碼來加深對此的理解。 首先咱們寫幾個簡單的語句,把一個變量賦值給另外一個變量,而且改變另外一個變量:數組

<?php
$a = 23;
$b = $a;
$b = 42;
var_dump($a); // int(23)
var_dump($b); // int(42)

這個腳本顯示 $a 值仍然爲 23  ,而 $b 則等於 42 。出現這個狀況的緣由是咱們獲得的是一個拷貝(具體發生了什麼稍後講解。。。)如今咱們使用引用來作一樣的事情:數據結構

<?php
$a = 23;
$b = &$a;
$b = 42;
var_dump($a); // int(42)
var_dump($b); // int(42)
?>

如今 $a 的值也改變成了 42 。 事實上,$a 和 $b 之間沒有任何區別,它們都使用了同一個變量容器(又名: zval )。 將這二者分開的惟一方法是使用 unset() 函數銷燬其中任何一個變量。函數

在 PHP 中,引用不只能用在普通語句中,還能用於函數參數和返回值:性能

<?php
function &foo(&$param) {
    $param = 42;
    return $param;
}

$a = 23;
echo "\$a before calling foo(): $a\n";
$b = foo($a);
echo "\$a after the call to foo(): $a\n";
$b = 23;
echo "\$a after touching the returned variable: $a\n";
?>

你認爲上面的結果是什麼呢?—— 沒錯,就像下面這樣:編碼

$a before calling foo(): 23
$a after the call to foo(): 42
$a after touching the returned variable: 42

這裏咱們初始化了一個變量,並把它做爲一個引用參數傳給了一個函數。函數改變了它,它有了新值。該函數返回同一個變量,咱們更改了返回的變量和它的原始值。。。 等等!它沒變,不是嗎!? —— 沒錯,可引用就是這樣。 具體發生了以下事情:該函數返回了一個引用,引用了 $a 的變量容器 zval,而且經過 = 賦值操做符爲它建立了一個副本。spa

爲了修復這個問題,咱們須要添加一個額外的 & 操做符:.net

$b = &foo($a);

結果和咱們所指望的同樣:設計

$a before calling foo(): 23
$a after the call to foo(): 42
$a after touching the returned value: 23

總結一下: PHP 的引用就是同一個變量的別名,想要正確的使用它們可能很難。想要詳細瞭解引用計數,這裏有份基礎資料,請參閱 手冊中的引用計數基本知識

PHP 5 發佈時最大的變更是『對象處理方式』。通常咱們理解爲:

在 PHP 4 中,對象被當成變量來對待,因此當對象做爲函數傳參時,他們是被複制的。但在 PHP 5 中,他們永遠是『引用傳參』。

以上的理解並不徹底正確。其主要目的是遵循『面對對象模式』:對象傳參給函數或者方法後,這個函數發送一個指令給對象(例如調用了一個方法)以此來改變對象的狀態(例如對象的屬性)。所以傳參進去的對象必須爲同一個。 PHP 4 的面對對象用戶使用『引用傳參』來解決這個問題,不過很難作到完美。PHP 5 引進了獨立於變量容器的『對象存儲器』。當一個對象賦值給變量時,變量再也不存儲整個對象(屬性表和其餘的『類』信息),而是存儲這個對象所在 存儲器的引用 —— 當咱們複製一個對象變量時,咱們複製的是這個『存儲器的引用』。這很容易被誤解爲『引用』,可是『存儲器的引用』與『引用』是徹底不一樣的概念。下面的示例代碼有助於咱們更好地區分:

<?php
// 建立一個對象和此對象的引用變量
$a = new stdclass;
$b = $a;
$c = &$a;

// 對『對象』進行操做
$a->foo = 42; 
var_dump($a->foo); // int(42)
var_dump($b->foo); // int(42)
var_dump($c->foo); // int(42)

// 如今直接改變變量的類型
$a = 42;
var_dump($a); // int(42)
var_dump($b); // object(stdClass)#1719 (1) {
              //            ["foo"]=>
              //            int(42)
              // }
var_dump($c); // int(42)
?>

以上代碼中,修改對象的屬性會影響到 複製 的變量 $b 和引用的變量 $c。可是在最後區塊的代碼中,當咱們修改 $a 的類型時,引用的 $c 發生了變化,而複製獲得的變量 $b 不會發生改變,這是個大多數有面對對象經驗的工程師所期待的。
So, 面對對象是惟一使用『引用』的理由,可是如今 PHP 4 已死,你也能夠放棄此類用法了。

另外一我的們使用『引用』的理由是 —— 這將讓代碼更快。可是這是錯誤的,引用並不會使代碼執行速度變快,更糟糕的是,不少時候『引用』會讓你的代碼執行效率更低。

我必須再鄭重強調一次:是的,不少時候『引用』會讓你的代碼執行效率更低。

別的語言的工程師,他們閱讀別的語言編碼規範,會看到建議在處理大的數據結構或者字串時,使用指針來減少對內存的消耗以提升運行效率。這些工程師誤將此概念理解到『引用』上,然而『指針』與『引用』是徹底不一樣的技術模型。PHP 解析器與其餘語言不一樣,在 PHP 中,咱們使用『寫時複製(copy-on-write)』模型。

在『寫時複製』模型裏,賦值和函數傳參不會觸發 複製 動做,你能夠理解爲多個不一樣的變量指向同一個『變量容器』,只有當『寫』動做發生時,纔會觸發複製動做。這意味着,即便變量看起來像是『複製』的,本質上卻不是。因此當傳參一個巨大的變量給某個函數時,並不會對性能形成多大影響。不過此時若是你使用引用傳參的話,引用傳參會關閉『寫時複製』機制,這會致使接下來那些沒有使用引用的變量傳參會被馬上覆制一份。這也不是世界末日,你也能夠在全部地方都引用就好了嘛。事實並不是如此:PHP 的內部機制依賴於『寫時複製』模型,存在不少你沒法修改的內部函數傳參。
我曾在某處看到過相似下面這樣的代碼:

<?php
function foo(&$data) {
    for ($i = 0; $i < strlen($data); $i++) {
        do_something($data{$i});
    }
}

$string = "... looooong string with lots of data .....";
foo(string);
?>

顯然,上面這段代碼的第一個問題是:在循環中調用 strlen() 而不是使用已經計算好的長度。也就是說調用一次 strlen($data) 就能夠了的,可是他卻調用了不少次。 不一樣於 C 這類語言, 通常來講,PHP 的字符串都自帶了長度,所以也不用進行長度的計算。因此就 strlen() 而言,這還不算太糟糕。 但如今另外一個問題是,案例中的這個開發者爲了節省時間,傳遞了一個引用做爲參數以顯示本身的聰明。 然而,strlen() 指望獲得的是一個副本。『寫時複製』不能用於引用,所以 $data 將會在 strlen() 調用時被複制,strlen() 將會作一個絕對簡單的操做 —— 事實上 strlen() 原本就是 PHP 裏最簡單的函數之一 —— 緊接着該副本就會被直接銷燬。

若是沒有使用引用,也就不必進行復制操做,代碼執行也會更快。並且就算 strlen() 支持引用,你也不會所以得到更多好處。

總的來講:

  • 除了 PHP4 的遺留問題,不要在面向對象(OO)中使用引用。
  • 不要使用引用來提高性能。

使用引用來完成事情的第三個問題是:經過參數的引用來返回數據所致使的糟糕的 API 設計。這個問題仍是由於那個開發者沒有意識到『PHP 就是 PHP 而不是其餘語言』所致使的。

在 PHP 中,同一個函數能夠返回不一樣數據類型。—— 所以,你能夠在函數執行成功時返回一個字符串,而在失敗時返回一個布爾值 false,PHP 也容許返回複雜的結構類型,好比數組和對象。因此在須要返回不少東西的時候,能夠將他們打包在一塊兒。另外,異常也是函數返回的一種方式。

使用引用是一件很差的事情,除了引用自己很差,而且還會使性能降低這個事實外,使用引用這種方式會使得代碼難以維護。像下面這段代碼的函數調用:

do_something($var);

你但願 $var 發生改變嗎?—— 固然不會。然而,若是 do_something() 傳遞的參數是引用,它就可能會改變。

這類 API 的另外一個問題是:函數不能鏈式調用,於是你總會遇到必須使用臨時變量的場景。鏈式調用可能會使可讀性下降,可是在許多場景下,鏈式調用使得代碼更加簡潔。

關於引用的糟糕的設計決定,我我的最喜歡的一個例子是 PHP 自帶的 sort() 函數。sort() 使用一個數組做爲引用參數,而後經過引用返回一個排好序的數組。 像常規那樣經過值返回一個排好序的數組可能還更好些。固然,這麼作是因爲歷史的緣由:sort() 比『寫時複製』更早出現。『寫時複製』產生於 PHP4,而 sort() 則更早,它早在 PHP 仍是做爲一種在 Web 上作起事來很方便的東西,而不是真正的成爲本身的語言的時候就存在了。

總之: 在 PHP 中,引用是很差的。 不要使用引用。 它們只會惹事生非,另外,不要對使用引用來提高引擎抱有但願。

更多現代化 PHP 知識,請前往 Laravel / PHP 知識社區
相關文章
相關標籤/搜索