去年我參加了不少次會議,其中八次會議裏我進行了相關發言,這其中我屢次談到了 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()
支持引用,你也不會所以得到更多好處。
總的來講:
使用引用來完成事情的第三個問題是:經過參數的引用來返回數據所致使的糟糕的 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 知識社區