摘要:發(fā)布時(shí)最大的變動(dòng)是對(duì)象處理方式。這很容易被誤解為引用,但是存儲(chǔ)器的引用與引用是完全不同的概念。使用引用是一件不好的事情,除了引用本身不好,并且還會(huì)使性能下降這個(gè)事實(shí)外,使用引用這種方式會(huì)使得代碼難以維護(hù)。
去年我參加了很多次會(huì)議,其中八次會(huì)議里我進(jìn)行了相關(guān)發(fā)言,這其中我多次談到了 PHP 的引用問(wèn)題,因?yàn)楹芏嗳藢?duì)它的理解有所偏差。在深入討論這個(gè)問(wèn)題之前,我們先回顧一下引用的基本概念,明確什么是“引用傳遞”。
在 PHP 中引用意味著用不同的名字訪問(wèn)同一個(gè)變量?jī)?nèi)容,不論你用哪個(gè)名字對(duì)變量做出了運(yùn)算,其他名字訪問(wèn)的內(nèi)容也將改變。
讓我們通過(guò)代碼來(lái)加深對(duì)此的理解。 首先我們寫(xiě)幾個(gè)簡(jiǎn)單的語(yǔ)句,把一個(gè)變量賦值給另一個(gè)變量,并且改變另一個(gè)變量:
這個(gè)腳本顯示?$a?值仍然為?23? ,而?$b?則等于?42 。出現(xiàn)這個(gè)情況的原因是我們得到的是一個(gè)拷貝(具體發(fā)生了什么稍后講解。。。)現(xiàn)在我們使用引用來(lái)做同樣的事情:
現(xiàn)在?$a?的值也改變成了?42 。 事實(shí)上,$a?和?$b?之間沒(méi)有任何區(qū)別,它們都使用了同一個(gè)變量容器(又名: zval )。 將這兩者分開(kāi)的唯一方法是使用?unset() 函數(shù)銷(xiāo)毀其中任何一個(gè)變量。
在 PHP 中,引用不僅能用在普通語(yǔ)句中,還能用于函數(shù)參數(shù)和返回值:
你認(rèn)為上面的結(jié)果是什么呢?—— 沒(méi)錯(cuò),就像下面這樣:
$a before calling foo(): 23 $a after the call to foo(): 42 $a after touching the returned variable: 42這里我們初始化了一個(gè)變量,并把它作為一個(gè)引用參數(shù)傳給了一個(gè)函數(shù)。函數(shù)改變了它,它有了新值。該函數(shù)返回同一個(gè)變量,我們更改了返回的變量和它的原始值。。。 等等!它沒(méi)變,不是嗎!? —— 沒(méi)錯(cuò),可引用就是這樣。 具體發(fā)生了如下事情:該函數(shù)返回了一個(gè)引用,引用了 $a 的變量容器 zval,并且通過(guò)?=?賦值操作符為它創(chuàng)建了一個(gè)副本。
為了修復(fù)這個(gè)問(wèn)題,我們需要添加一個(gè)額外的 & 操作符:
$b = &foo($a);結(jié)果和我們所期望的一樣:
$a before calling foo(): 23 $a after the call to foo(): 42 $a after touching the returned value: 23總結(jié)一下: PHP 的引用就是同一個(gè)變量的別名,想要正確的使用它們可能很難。想要詳細(xì)了解引用計(jì)數(shù),這里有份基礎(chǔ)資料,請(qǐng)參閱?手冊(cè)中的引用計(jì)數(shù)基本知識(shí) 。
PHP 5 發(fā)布時(shí)最大的變動(dòng)是『對(duì)象處理方式』。一般我們理解為:
在 PHP 4 中,對(duì)象被當(dāng)成變量來(lái)對(duì)待,所以當(dāng)對(duì)象作為函數(shù)傳參時(shí),他們是被復(fù)制的。但在 PHP 5 中,他們永遠(yuǎn)是『引用傳參』。以上的理解并不完全正確。其主要目的是遵循『面對(duì)對(duì)象模式』:對(duì)象傳參給函數(shù)或者方法后,這個(gè)函數(shù)發(fā)送一個(gè)指令給對(duì)象(例如調(diào)用了一個(gè)方法)以此來(lái)改變對(duì)象的狀態(tài)(例如對(duì)象的屬性)。因此傳參進(jìn)去的對(duì)象必須為同一個(gè)。 PHP 4 的面對(duì)對(duì)象用戶(hù)使用『引用傳參』來(lái)解決這個(gè)問(wèn)題,不過(guò)很難做到完美。PHP 5 引進(jìn)了獨(dú)立于變量容器的『對(duì)象存儲(chǔ)器』。當(dāng)一個(gè)對(duì)象賦值給變量時(shí),變量不再存儲(chǔ)整個(gè)對(duì)象(屬性表和其他的『類(lèi)』信息),而是存儲(chǔ)這個(gè)對(duì)象所在 存儲(chǔ)器的引用 —— 當(dāng)我們復(fù)制一個(gè)對(duì)象變量時(shí),我們復(fù)制的是這個(gè)『存儲(chǔ)器的引用』。這很容易被誤解為『引用』,但是『存儲(chǔ)器的引用』與『引用』是完全不同的概念。下面的示例代碼有助于我們更好地區(qū)分:
foo = 42; var_dump($a->foo); // int(42) var_dump($b->foo); // int(42) var_dump($c->foo); // int(42) // 現(xiàn)在直接改變變量的類(lèi)型 $a = 42; var_dump($a); // int(42) var_dump($b); // object(stdClass)#1719 (1) { // ["foo"]=> // int(42) // } var_dump($c); // int(42) ?>以上代碼中,修改對(duì)象的屬性會(huì)影響到 復(fù)制 的變量 $b 和引用的變量 $c。但是在最后區(qū)塊的代碼中,當(dāng)我們修改 $a 的類(lèi)型時(shí),引用的 $c 發(fā)生了變化,而復(fù)制得到的變量 $b 不會(huì)發(fā)生改變,這是個(gè)大多數(shù)有面對(duì)對(duì)象經(jīng)驗(yàn)的工程師所期待的。
So, 面對(duì)對(duì)象是唯一使用『引用』的理由,但是現(xiàn)在 PHP 4 已死,你也可以放棄此類(lèi)用法了。另一個(gè)人們使用『引用』的理由是 —— 這將讓代碼更快。但是這是錯(cuò)誤的,引用并不會(huì)使代碼執(zhí)行速度變快,更糟糕的是,很多時(shí)候『引用』會(huì)讓你的代碼執(zhí)行效率更低。
我必須再鄭重強(qiáng)調(diào)一次:是的,很多時(shí)候『引用』會(huì)讓你的代碼執(zhí)行效率更低。
別的語(yǔ)言的工程師,他們閱讀別的語(yǔ)言編碼規(guī)范,會(huì)看到建議在處理大的數(shù)據(jù)結(jié)構(gòu)或者字串時(shí),使用指針來(lái)減小對(duì)內(nèi)存的消耗以提高運(yùn)行效率。這些工程師誤將此概念理解到『引用』上,然而『指針』與『引用』是完全不同的技術(shù)模型。PHP 解析器與其他語(yǔ)言不同,在 PHP 中,我們使用『寫(xiě)時(shí)復(fù)制(copy-on-write)』模型。
在『寫(xiě)時(shí)復(fù)制』模型里,賦值和函數(shù)傳參不會(huì)觸發(fā) 復(fù)制 動(dòng)作,你可以理解為多個(gè)不同的變量指向同一個(gè)『變量容器』,只有當(dāng)『寫(xiě)』動(dòng)作發(fā)生時(shí),才會(huì)觸發(fā)復(fù)制動(dòng)作。這意味著,即使變量看起來(lái)像是『復(fù)制』的,本質(zhì)上卻不是。所以當(dāng)傳參一個(gè)巨大的變量給某個(gè)函數(shù)時(shí),并不會(huì)對(duì)性能造成多大影響。不過(guò)此時(shí)如果你使用引用傳參的話,引用傳參會(huì)關(guān)閉『寫(xiě)時(shí)復(fù)制』機(jī)制,這會(huì)導(dǎo)致接下來(lái)那些沒(méi)有使用引用的變量傳參會(huì)被立刻復(fù)制一份。這也不是世界末日,你也可以在所有地方都引用就行了嘛。事實(shí)并非如此:PHP 的內(nèi)部機(jī)制依賴(lài)于『寫(xiě)時(shí)復(fù)制』模型,存在很多你無(wú)法修改的內(nèi)部函數(shù)傳參。
我曾在某處看到過(guò)類(lèi)似下面這樣的代碼:顯然,上面這段代碼的第一個(gè)問(wèn)題是:在循環(huán)中調(diào)用 strlen() 而不是使用已經(jīng)計(jì)算好的長(zhǎng)度。也就是說(shuō)調(diào)用一次 strlen($data) 就可以了的,但是他卻調(diào)用了很多次。 不同于 C 這類(lèi)語(yǔ)言, 一般來(lái)說(shuō),PHP 的字符串都自帶了長(zhǎng)度,因此也不用進(jìn)行長(zhǎng)度的計(jì)算。所以就?strlen()?而言,這還不算太糟糕。 但現(xiàn)在另一個(gè)問(wèn)題是,案例中的這個(gè)開(kāi)發(fā)者為了節(jié)省時(shí)間,傳遞了一個(gè)引用作為參數(shù)以顯示自己的聰明。 然而,strlen()?期望得到的是一個(gè)副本?!簩?xiě)時(shí)復(fù)制』不能用于引用,因此?$data?將會(huì)在?strlen() 調(diào)用時(shí)被復(fù)制,strlen()?將會(huì)做一個(gè)絕對(duì)簡(jiǎn)單的操作 —— 事實(shí)上 strlen()?本來(lái)就是 PHP 里最簡(jiǎn)單的函數(shù)之一 —— 緊接著該副本就會(huì)被直接銷(xiāo)毀。
如果沒(méi)有使用引用,也就沒(méi)必要進(jìn)行復(fù)制操作,代碼執(zhí)行也會(huì)更快。而且就算 strlen() 支持引用,你也不會(huì)因此獲得更多好處。
總的來(lái)說(shuō):
除了 PHP4 的遺留問(wèn)題,不要在面向?qū)ο螅∣O)中使用引用。
不要使用引用來(lái)提升性能。
使用引用來(lái)完成事情的第三個(gè)問(wèn)題是:通過(guò)參數(shù)的引用來(lái)返回?cái)?shù)據(jù)所導(dǎo)致的糟糕的 API 設(shè)計(jì)。這個(gè)問(wèn)題還是因?yàn)槟莻€(gè)開(kāi)發(fā)者沒(méi)有意識(shí)到『PHP 就是 PHP 而不是其他語(yǔ)言』所導(dǎo)致的。
在 PHP 中,同一個(gè)函數(shù)可以返回不同數(shù)據(jù)類(lèi)型?!?因此,你可以在函數(shù)執(zhí)行成功時(shí)返回一個(gè)字符串,而在失敗時(shí)返回一個(gè)布爾值 false,PHP 也允許返回復(fù)雜的結(jié)構(gòu)類(lèi)型,比如數(shù)組和對(duì)象。所以在需要返回很多東西的時(shí)候,可以將他們打包在一起。另外,異常也是函數(shù)返回的一種方式。
使用引用是一件不好的事情,除了引用本身不好,并且還會(huì)使性能下降這個(gè)事實(shí)外,使用引用這種方式會(huì)使得代碼難以維護(hù)。像下面這段代碼的函數(shù)調(diào)用:
do_something($var);你希望 $var 發(fā)生改變嗎?—— 當(dāng)然不會(huì)。然而,如果?do_something()?傳遞的參數(shù)是引用,它就可能會(huì)改變。
這類(lèi) API 的另一個(gè)問(wèn)題是:函數(shù)不能鏈?zhǔn)秸{(diào)用,因而你總會(huì)遇到必須使用臨時(shí)變量的場(chǎng)景。鏈?zhǔn)秸{(diào)用可能會(huì)使可讀性降低,但是在許多場(chǎng)景下,鏈?zhǔn)秸{(diào)用使得代碼更加簡(jiǎn)潔。
關(guān)于引用的糟糕的設(shè)計(jì)決定,我個(gè)人最喜歡的一個(gè)例子是 PHP 自帶的 sort() 函數(shù)。sort()?使用一個(gè)數(shù)組作為引用參數(shù),然后通過(guò)引用返回一個(gè)排好序的數(shù)組。 像常規(guī)那樣通過(guò)值返回一個(gè)排好序的數(shù)組可能還更好些。當(dāng)然,這么做是由于歷史的原因:sort()?比『寫(xiě)時(shí)復(fù)制』更早出現(xiàn)?!簩?xiě)時(shí)復(fù)制』產(chǎn)生于 PHP4,而 sort() 則更早,它早在 PHP 還是作為一種在 Web 上做起事來(lái)很方便的東西,而不是真正的成為自己的語(yǔ)言的時(shí)候就存在了。
總之: 在 PHP 中,引用是不好的。 不要使用引用。 它們只會(huì)惹事生非,另外,不要對(duì)使用引用來(lái)提升引擎抱有希望。
更多現(xiàn)代化 PHP 知識(shí),請(qǐng)前往 Laravel / PHP 知識(shí)社區(qū)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/28494.html
摘要:大刀闊斧的改造在學(xué)習(xí)了兩遍之后,基于教程開(kāi)發(fā)的校園二手書(shū)交易平臺(tái)熊能本周閱讀清單紙牌屋弗蘭克知道的太晚了實(shí)現(xiàn)微信紅包拆分算法聊聊最近求職發(fā)生的故事無(wú)銘更多現(xiàn)代化知識(shí),請(qǐng)前往知識(shí)社區(qū) showImg(https://segmentfault.com/img/bV8ctF?w=1650&h=1100); 最新資訊 Laravel 5.6 中文文檔翻譯完成,譯者 60 人,耗時(shí) 10 天...
摘要:再往下就是我踩到的所有坑的記錄啦。第個(gè)坑作為一個(gè)已經(jīng)離不開(kāi)強(qiáng)大自動(dòng)補(bǔ)全的人,踩到的第一個(gè)坑是對(duì)的支持問(wèn)題。第個(gè)坑關(guān)于頁(yè)面跳轉(zhuǎn)間如何獲取當(dāng)前登錄的解決,然后任何都可以使用來(lái)訪問(wèn)當(dāng)前登錄的 因?yàn)橹暗木W(wǎng)站項(xiàng)目使用的是Spring MVC,而且當(dāng)時(shí)為了盡快趕完,代碼結(jié)構(gòu)非常粗暴,還存在大量的copy-paste代碼,然后被師兄批評(píng),然后決定接受師兄的建議,對(duì)網(wǎng)站進(jìn)行重構(gòu),并且使用聽(tīng)說(shuō)可以讓我...
摘要:缺點(diǎn)每次調(diào)用都有線程開(kāi)銷(xiāo)延遲初始化單例默認(rèn)構(gòu)造方法為,避免用戶(hù)用構(gòu)造出新對(duì)象獲取單例的靜態(tài)工廠同步方法延遲初始化單例使用同步方法保證多線程操作只實(shí)例化一個(gè)實(shí)力單例模式。 主要分為兩種: 直接初始化 延遲初始化 直接初始化 直接初始化final靜態(tài)成員 線程安全:JVM保證final靜態(tài)成員只會(huì)被初始化一次 公有靜態(tài)成員是個(gè)final域,直接引用成員獲取單例 /** * 公有靜態(tài)成...
摘要:利用這篇教程存儲(chǔ)一些常用的微信小程序開(kāi)發(fā)技巧,方便查找。但是第一,微信小程序是國(guó)內(nèi)的,有中文文檔,雖然它的文檔說(shuō)明有點(diǎn)坑,但好歹有文檔,閱讀理解對(duì)小伙伴們來(lái)說(shuō)不是問(wèn)題。 Create by jsliang on 2018-9-17 17:58:56 Recently revised in 2018-11-19 08:19:13 ?Hello 小伙伴們,如果覺(jué)得本文還不錯(cuò),記得給個(gè)...
摘要:利用這篇教程存儲(chǔ)一些常用的微信小程序開(kāi)發(fā)技巧,方便查找。但是第一,微信小程序是國(guó)內(nèi)的,有中文文檔,雖然它的文檔說(shuō)明有點(diǎn)坑,但好歹有文檔,閱讀理解對(duì)小伙伴們來(lái)說(shuō)不是問(wèn)題。 Create by jsliang on 2018-9-17 17:58:56 Recently revised in 2018-11-19 08:19:13 ?Hello 小伙伴們,如果覺(jué)得本文還不錯(cuò),記得給個(gè)...
閱讀 1172·2021-11-15 18:14
閱讀 3646·2021-11-15 11:37
閱讀 768·2021-09-24 09:47
閱讀 2453·2021-09-04 16:48
閱讀 2189·2019-08-30 15:53
閱讀 2390·2019-08-30 15:53
閱讀 400·2019-08-30 11:20
閱讀 1244·2019-08-29 16:08