為何說PHP引用是個坑,要慎用

2022-10-06 07:45:13 字數 4663 閱讀 1265

前言

去年我參加了很多次會議,其中八次會議裡我進行了相關發言,這其中我多次談到了 php 的引用問題,因為很多人對它的理解有所偏差。在深入討論這個問題之前,我們先回顧一下引用的基本概念,明確什麼是「引用傳遞」。

在 php 中引用意味著用不同的名字訪問同乙個變數內容,不論你用哪個名字對變數做出了運算,其他名字訪問的內容也將改變。

讓我們通過**來加深對此的理解。 首先我們寫幾個簡單的語句,把乙個變數賦值給另乙個變數,並且改變另乙個變數:

<?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)

$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,並且通過 = 賦值操作符為它建立了乙個副本。

為了修復這個問題,我們需要新增乙個額外的 & 操作符:

$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;

va程式設計客棧r_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)

var_dump($c); // int(42)

?>

以上**中,修改物件的屬性會影響到 複製 的變數 $b 和引用的變數 $c。但是在最後區塊的**中,當我們修改 $a 的型別時,引用的 $c 發生了變化,而複製得到的變數 $b 不會發生改變,這是個大多數有面對物件經驗的工程師所期待的。

so, 面對物件是唯一使用『引用』的理由,但是現在 php 4 已死,你也可以放棄此類用法了。

另乙個人們使用『引用』的理由是 —— 這將讓**更快。但是這是錯誤的,引用並不會使**執行速度變快,更糟糕的是,很多時候『引用』會讓你的**執行效率更低。

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

別的語言的工程師,他們閱讀別的語言編碼規範,會看到建議在處理大的資料結構或者字串時,使用指標來減小對記憶體的消耗以提高執行效率。這些工程師誤將此概念理解到『引用』上,然而『指標』與『引用』是完全不同的技術模型。php 解析器與其他語言不同,在 php 中,我們使用『寫時複製(copy-on-write)』模型。

在『寫時複製』模型裡,賦值和函式傳參不會觸發 複製 動作,你可以理解為多個不同的變數指向同乙個『變數容器』,只有當『寫』動作發生時,才會觸發複製動作。這意味著,即使變數看起來像是『複製』的,本質上卻不是。所以當傳參乙個巨大的變數給某個函式時,並不會對效能造成多大影響。不過此時如果你使用引用傳參的話,引用傳參會關閉『寫時複製』機制,這會導致接下來那些沒有使用引用的變數傳參會被立刻複製乙份。這也不是世界末日,你也可以在所有地方都引用就行了嘛。事實並非如此:php 的內部機制依賴於『寫時複製』模型,存在很多你無法修改的內部函式傳參。

我曾在某處看到過類似下面這樣的**:

<?php function foo(&$data) ); }}

$struenrqing = "... 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引用是個坑,要慎用

本文位址: /wangluo/php/224261.html

PHP 引用是個壞習慣

在寫php 程式的時候,很多人在傳遞引數的時候,喜歡用乙個引用。特別是在乙個陣列非常的大的時候,更是喜歡加。function binsearch arr key value elseif value item else return false 在這裡,mid 採用了先減後加的方法計算,目的是為了防...

PHP 引用是個壞習慣

在寫php 程式的時候,很多人在傳遞引數的時候,喜歡用乙個引用。特別是在乙個陣列非常的大的時候,更是喜歡加。複製 如下 function binsearch arr,key,value else if value item else return false 在這裡,mid 採用了先減後加的方法計算...

PHP 引用是個壞習慣

這篇文章,寫的比較早,有一些錯誤的說法,我表示非常歉意。一些 沒有經過嚴格的測試,以後寫部落格一定要非常謹慎。因為可能會對一些人產生誤導。在寫php 程式的時候,很多人在傳遞引數的時候,喜歡用乙個引用。特別是在乙個陣列非常的大的時候,更是喜歡加。function binsearch arr key ...