第3章 PHP程序調(diào)優(yōu)
某書提出一個(gè)觀點(diǎn)「代碼質(zhì)量與其整潔度成正比」。確實(shí),干凈的代碼既在質(zhì)量上較為可靠,也方便閱讀,為后期維護(hù)、重構(gòu)奠定了良好基礎(chǔ)。 3.1 代碼提煉封裝 當(dāng)代碼太多的時(shí)候,我們常常會(huì)忽略掉以前我們所做的工作,使得我們不得不再做一次同樣的工作。提煉封裝能幫我們比較好的管理這些代碼。 3.1.1 提取變量
1. 變量封裝
將僅限于本類使用的變量使用 private 修飾,并提供對(duì)應(yīng)的訪問(wèn)方法,可以將與外部調(diào)用者無(wú)關(guān)的變量隱藏起來(lái),減少代碼的耦合性與出錯(cuò)的概率。 例如 : class Singleton { ??public $instance = ...; } 私有化 $instance 屬性,并添加 getInstance/setInstance 方法管理 : class Singleton { ??private $instance = ...; ? ??public function getInstance() ?{ return $this->instance; ?} ? ??public function setInstance($instance) ?{ $this->instance = $instance; ?} } 注意:并不是需要把所有的成員變量都加以 private 修飾,這樣只會(huì)適得其反,需要根據(jù)變量的需求來(lái)進(jìn)行封裝。如果你的成員變量不需要被外部直接使用,就放心的使用 private 吧。
2. 變量提煉
給函數(shù)、類一個(gè)好的命名可以減少我們一些理解時(shí)間,并且代碼也會(huì)更好看,變量也是同樣。例 : if (($user->toUpperCase()->indexOf("AGE") > 18) && ($user->toUpperCase()->indexOf("SEX") == 'male')) { ??# 代碼塊 } 這種方式很難知道需要判斷的是什么,在字段無(wú)法精準(zhǔn)表達(dá)的時(shí)候就會(huì)更難說(shuō)明。建議使用下面方式 : $isEnoughAge = $user->toUpperCase()->indexOf("AGE") > 18; $isMale = $user->toUpperCase()->indexOf("SEX") == 'male'; ? if ($isEnoughAge && $isMale) { ??# 代碼塊 } 將小括號(hào)中的表達(dá)式提取出來(lái)賦予適合的變量,通過(guò)變量的名稱即可了解表達(dá)含義。 提煉變量的主要原因是通過(guò)將復(fù)雜表達(dá)式給予適合的名稱,從而使其更易于理解。當(dāng)然并不是所有的表達(dá)式都需要提出,因?yàn)檫^(guò)多的提出會(huì)導(dǎo)致變量增多。當(dāng)你覺得表達(dá)式很難表達(dá)你的意思,那就該提煉變量出場(chǎng)了。
3. 參數(shù)替代
當(dāng)調(diào)用一個(gè)函數(shù)時(shí),需要傳遞一長(zhǎng)~~ 串的參數(shù),這樣是不合理的。我們應(yīng)該把這一長(zhǎng)串參數(shù)用更少的參數(shù)替代 - 如果可以一個(gè)對(duì)象最好。 例如 : function enter($name, $clothes, $age, $sex...) { # 代碼塊 } ? enter('Jack', '西服', 16, 'male'); 我們應(yīng)該提取其中的參數(shù)作為一個(gè)對(duì)象傳遞 : Class Person { ??public $name = 'Jack'; ??public $clothes = '西服'; ??public $age = 16; ??public $sex = 'male'; } ? function enter(Person $person) { # 代碼塊 } ? enter(new Person()); 將需要使用的變量放到一個(gè)對(duì)象中,通過(guò)傳遞該對(duì)象來(lái)使用這些參數(shù)。 3.1.2 提煉函數(shù) 提煉函數(shù) - 將大段代碼中的一部分提取后或?qū)⒍鄠€(gè)方法中公用的部分提供出來(lái),構(gòu)成一個(gè)新方法。這樣可以使整段程序的結(jié)構(gòu)變得更清晰、增加可讀性、擴(kuò)展性和可維護(hù)性。 當(dāng)有兩個(gè)很類似的方法 : function choose1() { ??return phone('某米'); } ? function choose2() { ??return phone('某為'); } 我們可以提取其中共同的部分作為一個(gè)獨(dú)立的函數(shù) : function choose($phone) { ??return phone($phone); } ? function choose1() { ??return choose('某米'); } ? function choose2() { ??return choose('某為'); } 這里看起來(lái)是變得更麻煩了,但是實(shí)際開發(fā)我們不可能只會(huì)有這么一些,另外我們?cè)谛薷墓δ軙r(shí)只需改變其核心部分而不需要每個(gè)都改變。 注意:每個(gè)創(chuàng)建的函數(shù)都需要被調(diào)用至少 1 次。
3.1.3 提煉類
1. 方法上移
將多個(gè)類所公用的部分提取出來(lái),形成一個(gè)新的類,這個(gè)類被稱為它們的基類或者父類。子類可以根據(jù)基類的成員結(jié)合自己的需求去完成工作。基類一般不會(huì)直接實(shí)例化使用,所以基類一般是抽象類。 當(dāng)兩個(gè)類的成員類似時(shí) : class Boy { ??protected $name = 'Jack'; ? ??public function getName() ?{ return $this->name; ?} ? ??public function setName($name) ?{ $this->name = $name; ?} } ? class Girl { ??protected $name = 'Alice'; ? ??public function getName() ?{ return $this->name; ?} ? ??public function setName($name) ?{ $this->name = $name; ?} } 我們可以提取其中共同的部分作為基類 : abstract Class Base { ??protected $name = ''; ? ??public function getName() ?{ return $this->name; ?} ? ??public function setName($name) ?{ $this->name = $name; ?} } ? class Boy extends Base { ??protected $name = 'Jack'; } ? class Girl extends Base { ??protected $name = 'Alice'; } 除了提取為基類外,還可以把類的共同部分提取出來(lái)作為“供應(yīng)商”,當(dāng)有需要時(shí)直接通過(guò)該類的對(duì)象獲取對(duì)應(yīng)數(shù)據(jù)即可,而不需要繼承該類。當(dāng)然,凡事有利有弊,需要根據(jù)實(shí)際情況判斷是否使用。
2. 方法下移
方法下移和方法上移就剛好相反了,上移是提取出共同的部分上移給基類,而下移則是把基類中只為個(gè)別子類 “開小灶” 的方法移到對(duì)應(yīng)子類。 例 - 女孩不太喜歡玩耍,只想好好學(xué)習(xí) : abstract Class Base { ??public function teach() ?{ # 教你做人 ?} ? ??public function play() ?{ # 和你玩耍 ?} } ? class Boy extends Base { ??# 兩種方法都會(huì)用 } ? class Girl extends Base { ??# 用不到 play 方法 } 這時(shí) play 方法默認(rèn)會(huì)被 Girl 類繼承。需要做如下改動(dòng) : abstract Class Base { ??public function teach() ?{ # 教你做人 ?} } ? class Boy extends Base { ??public function play() ?{ # 和你玩耍 ?} } ? class Girl extends Base { } 這就完成了方法下移了。
3. 移除委托
當(dāng)需要另一個(gè)對(duì)象幫你完成某個(gè)功能時(shí),你卻找了一個(gè)中間人來(lái)橫插一腳。我們需要判斷中間人是否有存在的必要,如果沒有必要?jiǎng)t需要移除該委托。 例 - Jack 需要看時(shí)間,通過(guò) Alice 查看 : Class Jack { ??public function checkTime() ?{ (new Alice())->checkTime(); ?} } ? class Alice { ??public function checkTime() ?{ (new Clock())->getTime(); ?} } ? class Clock { ??public function getTime() ?{ # 當(dāng)前事件 ?} } 此時(shí) Alice 則沒有存在的必要,可以刪除 : Class Jack { ??public function checkTime() ?{ (new Clock())->getTime(); ?} } ? class Clock { ??public function getTime() ?{ # 當(dāng)前事件 ?} } 3.2 識(shí)別壞代碼 3.2.1 Bloaters Bloaters 是代碼、方法和類已經(jīng)增加到很多行代碼,這時(shí)候它們已經(jīng)很難使用了。這些代碼并不會(huì)立馬凸顯問(wèn)題,但是隨著代碼不斷改變,問(wèn)題就會(huì)越來(lái)越明顯。識(shí)別壞代碼,有助于發(fā)現(xiàn)代碼的潛在問(wèn)題,從而可以有的放矢的修改現(xiàn)有代碼,使之不斷完善。
1. Long Method(過(guò)長(zhǎng)函數(shù))
特征:一個(gè)方法含有太多行代碼。一般來(lái)說(shuō),任何方法超過(guò)10行時(shí),你就可以考慮是不是過(guò)長(zhǎng)了。 原因:在一個(gè)原有功能上面添加功能更方便,多次在同一個(gè)方法中添加,該方法就會(huì)變大并且不易修改。 策略 : 通過(guò)「提煉方法」將過(guò)長(zhǎng)的函數(shù)按照功能的不同進(jìn)行適當(dāng)拆解為小函數(shù)。例如:注釋、循環(huán)等就是提煉很好的地方。
給函數(shù)取一個(gè)好的名字,通過(guò)名字來(lái)了解函數(shù)提供的功能,提高代碼的理解性。
2. Large Class(過(guò)大類)
特征:一個(gè)類包含過(guò)多的字段、方法、代碼行。 原因:類似過(guò)長(zhǎng)函數(shù)。 策略 : 如果過(guò)大類中的部分行為可以提煉到一個(gè)獨(dú)立的組件中,可以使用「提煉類」。
如果有必要為客戶端提供一組操作和行為,可以使用「提煉接口」。
3. Primitive Obsession(基本類型偏執(zhí))
特征:總是使用基本類型而不使用對(duì)象。 原因:開始寫程序時(shí)可能涉及的字段不多,在日益增加的功能中,字段越來(lái)越多,基本類型也越來(lái)越多。 策略:使用「提煉類」或 使用已存在的對(duì)象取代這些參數(shù),實(shí)現(xiàn)使用對(duì)象代替基本型數(shù)據(jù)項(xiàng)目。
4. Long Parameter List(過(guò)長(zhǎng)參數(shù)列)
特征:一個(gè)方法有超過(guò)三四個(gè)的參數(shù)。 原因:可能是將多個(gè)算法并到一個(gè)函數(shù)中時(shí)發(fā)生的。函數(shù)中的入?yún)⒖梢杂脕?lái)控制最終選用哪個(gè)算法去執(zhí)行。 策略: 如果向已有的對(duì)象發(fā)出一條請(qǐng)求就可以取代一個(gè)參數(shù),則可通過(guò)「以函數(shù)取代參數(shù)」移除參數(shù)列,通過(guò)在函數(shù)內(nèi)部向上述已存在的對(duì)象查詢來(lái)獲取參數(shù)。
如果某些數(shù)據(jù)缺乏合理的對(duì)象歸屬,可使用「 引入?yún)?shù)對(duì)象」為它們制造出一個(gè)參數(shù)對(duì)象。
5. Data Clumps(數(shù)據(jù)泥團(tuán))
特征:代碼的不同部分包含相同的變量組 - 簽名、數(shù)據(jù)庫(kù)連接信息等 。 原因:復(fù)制粘貼或設(shè)計(jì)不當(dāng)導(dǎo)致。 策略:通過(guò)使用「創(chuàng)建新的參數(shù)對(duì)象取代這些參數(shù)」或「使用已存在的對(duì)象取代這些參數(shù)」,實(shí)現(xiàn)使用對(duì)象代替類成員屬性和方法中參數(shù)列表,清除數(shù)據(jù)泥團(tuán),使代碼簡(jiǎn)潔,也提高維護(hù)性和易讀性。 一個(gè)好的評(píng)斷辦法是:刪掉眾多數(shù)據(jù)中的一筆,其它數(shù)據(jù)有沒有因此失去意義。如果不在有意義則應(yīng)該為其生成一個(gè)新的對(duì)象了。
3.2.2 Object-Orientation Abusers Object-Orientation Abusers 是因?yàn)槊嫦驅(qū)ο缶幊淘瓌t不完整或不正確的應(yīng)用。
1. Switch Statements(switch 語(yǔ)句)
特征:同樣的switch語(yǔ)句出現(xiàn)在不同的方法或不同的類中,這樣當(dāng)需要增加新的 case 分支或者修改時(shí),就必須找到所有的地方,然后進(jìn)行修改。 原因:對(duì) switch 語(yǔ)句的依賴。 策略:大多數(shù)時(shí)候,一看到 switch 語(yǔ)句,就應(yīng)該考慮以多態(tài)來(lái)替換它。 switch 語(yǔ)句常常根據(jù)類型碼進(jìn)行選擇,你要的是 “與該類型碼相關(guān)的函數(shù)或類”,所以應(yīng)該運(yùn)用「提煉函數(shù)」將 switch 語(yǔ)句提煉到一個(gè)獨(dú)立函數(shù)中,再以「搬移函數(shù)」將它搬移到需要多態(tài)性的那個(gè)類里。
如果你的 switch 是基于類型碼來(lái)識(shí)別分支,這時(shí)可以運(yùn)用「以子類取代類型碼」或「以策略模式取代類型碼」。
一旦完成這樣的繼承結(jié)構(gòu)后,就可以運(yùn)用「以多態(tài)取代條件表達(dá)式」 了。
如果條件分支并不多并且它們使用不同參數(shù)調(diào)用相同的函數(shù),多態(tài)就沒必要了。在這種情況下,你可以運(yùn)用「以明確函數(shù)取代參數(shù)」。
如果你的選擇條件之一是 null,可以運(yùn)用「引入 Null 對(duì)象」。
2. Temporary Field(臨時(shí)字段)
特征:某些只在特定環(huán)境下有意義的字段。 原因:當(dāng)存儲(chǔ)一個(gè)數(shù)據(jù)卻沒有比較好的容器。 策略:通過(guò)「提煉類」將這些臨時(shí)字段及其相關(guān)的函數(shù)移植的一些新的類中。
3. Refused Bequest(被拒絕的遺贈(zèng))
特征:子類應(yīng)該繼承父類的函數(shù)和數(shù)據(jù)。但如果它們不想或不需要繼承,這就意味繼承體系設(shè)計(jì)錯(cuò)誤。 原因:有人只是為了重用超類中的代碼而創(chuàng)建子類。但是超類和子類是完全不同的。 策略: 如果繼承有意義,可以為子類創(chuàng)建一個(gè)兄弟類,將所有超類中對(duì)于子類無(wú)用的字段和函數(shù)提取到兄弟類中;
如果繼承沒有意義并且子類和父類之間確實(shí)沒有共同點(diǎn),可以運(yùn)用「以委托取代繼承」消除繼承。
4. Alternative Classes with Different Interfaces(異曲同工的類)
特征:兩個(gè)類中有著不同的函數(shù),卻在做著同一件事。 原因:創(chuàng)建這個(gè)類的程序員并不知道已經(jīng)有實(shí)現(xiàn)這個(gè)功能的類存在。 策略 : 使用「重命名方法」,根據(jù)他們的用途來(lái)重命名。
可以適當(dāng)運(yùn)用「搬移函數(shù)」,使類與接口保持一致。
如果僅有部分一致,可以使用「提煉超類」,可以讓已存在的類作為超類。
3.2.3 Change Preventers Change Preventers 意味著如果需要在代碼的一個(gè)地方更改某些內(nèi)容,就必須在其他地方也進(jìn)行許多更改。因此,程序開發(fā)變得更加復(fù)雜和代價(jià)更大。
1. Divergent Change(發(fā)散式變化)
特征:當(dāng)你對(duì)類進(jìn)行更改時(shí),發(fā)現(xiàn)不得不修改更多不相關(guān)的方法。 原因:復(fù)制粘貼或編程結(jié)構(gòu)不合理導(dǎo)致。 策略:將每次因同一條件變化,而需要同時(shí)修改的若干方法通過(guò)「提煉類」將它們提煉到一個(gè)新類中。
2. Shotgun Surgery(霰彈式修改)
特征:任何修改都需要在許多不同類上做小幅度修改。 原因:?jiǎn)我坏穆氊?zé)的拆分。 策略 : 使用「移動(dòng)函數(shù)」和「移動(dòng)字段」將不同類中相同的行為到一個(gè)獨(dú)立類中。 如果沒有合適的類則創(chuàng)建一個(gè)新類。
通常,可以運(yùn)用「將類內(nèi)聯(lián)化」將一些列相關(guān)行為放進(jìn)同一個(gè)類。
對(duì)比發(fā)散式變化與霰彈式修改:發(fā)散式變化是指一個(gè)類受多種變化的影響。霰彈式修改是指一個(gè)變化引發(fā)多個(gè)類相應(yīng)的修改。
3. Parallel Inheritance Hierarchies(平行繼承體系)
特征:為某個(gè)類添加一個(gè)子類,必須同時(shí)為另一個(gè)類相應(yīng)添加一個(gè)子類。是霰彈式修改 的特殊情況(比較少見)。 原因:隨著新類的加入,繼承體系越來(lái)越大。 策略:在一個(gè)類繼承體系的對(duì)象中引用另一個(gè)類繼承體系的對(duì)象,然后運(yùn)用「移動(dòng)方法」和「移動(dòng)字段」將被引用類中的一些方法和成員變量遷移宿主類中,消除被引用類的繼承體系。 3.2.4 Dispensables 可有可無(wú)的東西往往不能帶給我們有效的幫助,反而會(huì)增加工作壓力。
1. Comments(過(guò)多的注釋)
特征:這個(gè)也是壞代碼嗎?注釋本身不是壞事,但是如果代碼能夠表達(dá)其意則沒有必要過(guò)多注釋。 原因:代碼含義不直觀,想要通過(guò)注釋說(shuō)明。 策略: 如果需要說(shuō)明一段代碼功能,可以試試「提取方法」,提取出一個(gè)獨(dú)立的函數(shù),讓函數(shù)名稱解釋該函數(shù)的用途。
如果覺得需要注釋來(lái)說(shuō)明系統(tǒng)的某些假設(shè)條件,也可嘗試使用「引入斷言」,來(lái)明確標(biāo)明這些假設(shè)。
當(dāng)你感覺需要撰寫注釋時(shí),請(qǐng)先嘗試重構(gòu),試著讓所有的注釋都變得多余。
注意:以上之外還有注釋請(qǐng)簡(jiǎn)潔針對(duì)性編寫注釋,拒絕長(zhǎng)篇大論。
2. Duplicate Code(重復(fù)代碼)
特征:如果你在一個(gè)以上的地方看到相同的代碼,那么可以設(shè)法將它們合而為一。 原因:多個(gè)程序員同時(shí)在同一程序的不同部分上工作時(shí),以及特定部分的代碼看上去不同但實(shí)際在做同一件事。 策略: 在同一個(gè)類的不同地方:通過(guò)采用「提取方法」提煉出重復(fù)的代碼,然后在這些地方調(diào)用上述提煉出方法。
在不同子類中:通過(guò)「提取方法」提煉出重復(fù)的代碼,然后將該方法移動(dòng)到上級(jí)的父類內(nèi)。
在沒有關(guān)系的類中:通過(guò)對(duì)其中一個(gè)使用「提取類」 將重復(fù)的代碼提煉到一個(gè)新類中,然后在另一個(gè)類中調(diào)用生成的新類,消除重復(fù)的代碼。
3. Lazy Class(冗余類)
特征:一些類由于種種原因已經(jīng)不再承擔(dān)足夠責(zé)任,當(dāng)其價(jià)值低于維護(hù)價(jià)值則應(yīng)該刪除了。 原因:隨著代碼的變遷類被需要的越來(lái)越少。 策略:通過(guò) 「折疊繼承體系」,將這些冗余的類合并到父類或子類中,或者通過(guò)「將類內(nèi)聯(lián)化」,將這些冗余類中的所有函數(shù)、字段移到其他相關(guān)的類中。
4. Data Class(純稚的數(shù)據(jù)類)
特征:類擁有字段以及用來(lái)訪問(wèn)字段的 getter 和 setter 函數(shù),但是沒有其他的功能函數(shù)。 原因:只用對(duì)象存儲(chǔ)個(gè)別字段。 策略:找出其他類中訪問(wèn)數(shù)據(jù)類中的 getter/setter 的函數(shù),嘗試以「移動(dòng)方法」將這些函數(shù)移植到數(shù)據(jù)類中,實(shí)現(xiàn)將數(shù)據(jù)和操作行為裝在一起,也讓數(shù)據(jù)類承擔(dān)一定的責(zé)任。
5. Dead Code(死碼)
特征:變量、參數(shù)、字段、方法或類等不再使用(通常是因?yàn)樗鼈冞^(guò)時(shí)了)。 原因:當(dāng)對(duì)程序的需求發(fā)生變化或做出更正時(shí),沒有人有時(shí)間清理舊代碼。 策略 : 找到死代碼的最快方法是使用一個(gè)好的 IDE。
刪除未使用的代碼和不需要的文件。
6. Speculative Generality(夸夸其談未來(lái)性)
特征:存在未被使用的類、函數(shù)、字段或參數(shù)。 原因:準(zhǔn)備為未來(lái)提供更多可能,但是一直沒有行動(dòng)。 策略 : 如果抽象類作用不大,使用「折疊繼承體系」合并抽象類。
委托類作用不大,使用「將類內(nèi)聯(lián)化」移除非必要的委托 。
函數(shù)中有無(wú)用參數(shù),使用「移除參數(shù)」刪除多余的參數(shù)。
3.2.5 Couplers Couplers 下的所有壞代碼都會(huì)導(dǎo)致類之間的過(guò)度耦合以及過(guò)度委托。
1. Feature Envy(依戀情結(jié))
特征:一個(gè)函數(shù)訪問(wèn)其它對(duì)象的數(shù)據(jù)比訪問(wèn)自己的數(shù)據(jù)更多。 原因:在將字段移動(dòng)到數(shù)據(jù)類后,可能會(huì)發(fā)生這種情況。 策略:使用「移動(dòng)方法」將這些方法移動(dòng)到對(duì)應(yīng)的類中,以化解其 “相思之苦 ”,以 “少數(shù)服從多數(shù)” 的原則為主。
2. Inappropriate Intimacy(狎昵關(guān)系)
特征:兩個(gè)類的聯(lián)系過(guò)于緊密,使用了大量彼此的成員。 原因:類沒有合理設(shè)計(jì)。 策略: 采用「移動(dòng)方法」和「移動(dòng)字段」來(lái)減少關(guān)聯(lián)。
運(yùn)用「將雙向關(guān)聯(lián)改為單向關(guān)聯(lián)」,降低類之間過(guò)多的依存。
過(guò)提取類兩個(gè)類之間的共同點(diǎn)移植到一個(gè)新的類中,通過(guò)新的類來(lái)牽線搭橋。
3. Message Chains(過(guò)度耦合的消息鏈)
特征:在調(diào)用一個(gè)對(duì)象時(shí),這個(gè)對(duì)象往往會(huì)調(diào)用另一個(gè)對(duì)象,被調(diào)用對(duì)象又會(huì)繼續(xù)調(diào)用其他......。 原因:編寫代碼時(shí)怎么樣寫方便怎么來(lái),導(dǎo)致需要的方法在不同對(duì)象。 策略:觀察消息鏈最終得到的對(duì)象是用來(lái)干什么的,看看能否以提取方法的方式把使用該對(duì)象的代碼提煉到一個(gè)獨(dú)立函數(shù)中,再運(yùn)用移動(dòng)方法把這個(gè)函數(shù)推入消息鏈 。
4. Middle Man(中間人)
特征:我們經(jīng)常會(huì)看到某個(gè)類有一半的函數(shù)都委托給其他類,這樣就是過(guò)度委托。 原因:封裝 - 對(duì)外部隱藏內(nèi)部細(xì)節(jié),通常封裝會(huì)伴隨著委托。委托過(guò)多久成了過(guò)度委托。 策略:當(dāng)委托過(guò)多時(shí),應(yīng)該「移除中間委托」直接和負(fù)責(zé)對(duì)象交接。 3.2.6 Other Smells 其他的壞代碼,暫無(wú)更多。
Incomplete Library Class(不完美的程序庫(kù)類)
特征:當(dāng)一個(gè)類庫(kù)已經(jīng)不能滿足實(shí)際需要時(shí),你就不得不改變這個(gè)庫(kù)。 原因:類的作者沒有未卜先知的能力,因此庫(kù)往往構(gòu)造的不夠好。 策略 : 如果可以修改直接修改最好。
如果無(wú)法直接修改,并且只想修改其中的一兩個(gè)函數(shù),可以采用「引入外加函數(shù)」策略,以外加函數(shù)的方式來(lái)實(shí)現(xiàn)一項(xiàng)新功能。
如果需要建立大量的額外函數(shù),可應(yīng)該采用「引入本地?cái)U(kuò)展」建立一個(gè)新類。
3.3 常見的代碼優(yōu)化細(xì)節(jié) 3.3.1 變量細(xì)節(jié)
1. 銷毀變量
對(duì)于 global 變量應(yīng)該用完就銷毀掉,可以使用 unset 函數(shù)銷毀。
2. 數(shù)組元素
數(shù)組的 key 添加引號(hào),速度會(huì)比不加快。當(dāng)不加時(shí)默認(rèn)會(huì)先作為常量然后才當(dāng)做字符串。
3. 謹(jǐn)慎聲明全局變量
聲明一個(gè)未被任何函數(shù)使用的全局變量,性能也會(huì)降低,所以謹(jǐn)慎聲明。
4. 引號(hào)
單引號(hào)的性能優(yōu)于雙引號(hào),雙引號(hào)會(huì)優(yōu)先解析所包裹的內(nèi)容。
5. 變量復(fù)制
如非必要,不要為了方便或者整潔就把一個(gè)變量復(fù)制到另一個(gè)變量中。例如 $password = $_POST['password']。
6. 多維數(shù)組
多維數(shù)組盡量不要循環(huán)嵌套賦值。 3.3.2 函數(shù)細(xì)節(jié)
1. echo 與 print
echo 效率高于 print,echo 是沒有返回值的。
2. require_once/include_once
相對(duì)于 require_once/include_once 更推薦使用 require/include,因?yàn)?once 需要判斷一次。
3. 引入路徑
推薦使用絕對(duì)路徑,因?yàn)橄鄬?duì)路徑會(huì)在 include_path 里遍歷查找文件。
4. 時(shí)間獲取
如果需要獲取腳本執(zhí)行時(shí)間,$_SERVER['REQUEST_TIME'] 優(yōu)于 time( )。
5. 正則表達(dá)式
能不用正則表達(dá)式盡量不用,正則表達(dá)式很耗性能,匹配的越多越耗性能。
6. 替換
替換字符串之前,先使用 strpos 查找是否存在(非??欤L鎿Q函數(shù) str_replace 優(yōu)于 preg_replace。
7. 參數(shù)
當(dāng)參數(shù)不多,并且一個(gè)函數(shù)既能接受數(shù)組又能接受字符串,優(yōu)先傳遞字符串。
8. 局部變量
在函數(shù)內(nèi)創(chuàng)建局部變量調(diào)用最快,能作為局部變量?jī)?yōu)先局部。
9. 提前聲明局部變量
如果使用一個(gè)變量時(shí),使用一個(gè)已經(jīng)定義過(guò)的局部變量比建立一個(gè)未聲明的局部變量快,所以建議優(yōu)先定義再使用。
10. echo
使用 echo 時(shí),‘,’ 的性能優(yōu)于 ‘.’。注意:echo 是語(yǔ)言結(jié)構(gòu)而不是真正的函數(shù)。
11. isset 代替 strlen
在某些時(shí)候,可以使用 isset 代替 strlen(strlen 也足夠快)。
12. file_get_contents
可以用 file_get_contents( ) 替代 file( )、fopen( )、feof( )、fgets( ) 等方法的情況下,盡量用 file_get_contents( ),因?yàn)樗男矢叩枚唷?
13. 預(yù)定義函數(shù)
如果可以,盡量使用 PHP 內(nèi)部提供的函數(shù)。
14. split 與 explode
split 比 explode 快,盡量?jī)?yōu)先運(yùn)用 split。 3.3.3 類細(xì)節(jié)
1. 盡量靜態(tài)化
如果一個(gè)方法可以被靜態(tài)化,那就盡量聲明為靜態(tài)的。 靜態(tài)方法和非靜態(tài)方法的效率主要區(qū)別在內(nèi)存:靜態(tài)方法在程序開始時(shí)生成內(nèi)存,非靜態(tài)方法在程序運(yùn)行中生成內(nèi)存,靜態(tài)速度會(huì)快但是也會(huì)占內(nèi)存。
2. 魔術(shù)方法
避免使用類似 __get、__set 等魔術(shù)方法。
3. 類與方法
類的性能和方法個(gè)數(shù)沒有關(guān)系。
4. 方法所屬
同樣的方法,在子類中的性能優(yōu)于在父類中。
5. 函數(shù)與方法
同樣功能,調(diào)用函數(shù)性能優(yōu)于調(diào)用類方法。
6. 數(shù)據(jù)庫(kù)操作
php 操作數(shù)據(jù)庫(kù)不建議使用 mysql,而建議使用 增強(qiáng)型的 mysqli 或者 pdo。
7. 面向?qū)ο?/p>
面向?qū)ο蟮拈_銷相對(duì)較大,所以并不是所有的都要面向?qū)ο蟆?3.3.4 其他細(xì)節(jié)
1. 循環(huán)最大值
如果在循環(huán)中有最大值,需要在循環(huán)之前就設(shè)置好。
2. 循環(huán)變量
循環(huán)內(nèi)部不要聲明變量,尤其是大的變量,比如 對(duì)象。
3. 最好不用 @
用 @ 掩蓋錯(cuò)誤會(huì)降低腳本運(yùn)行速度,并且在后臺(tái)有很多額外操作。
4. 循環(huán)中的函數(shù)
盡量不要在循環(huán)中使用函數(shù),例如通過(guò) count 統(tǒng)計(jì)數(shù)組的元素個(gè)數(shù),可以放到循環(huán)外部,直接使用統(tǒng)計(jì)的值,避免每次都統(tǒng)計(jì)一次。
5. 三元運(yùn)算符
如果可以,優(yōu)先使用三元運(yùn)算符。
6. 自增
使用自增時(shí),++$i 會(huì)筆記 $i++ 快一些。
7. 分支語(yǔ)句
switch - case 好于使用多個(gè) if - else if 語(yǔ)句,并且代碼更加容易閱讀和維護(hù)。
8. foreach
foreach 效率更高,所以盡量使用 foreach 代替 for 或者 while 循環(huán)。 并不是說(shuō)遇到上述情況就一定要改變,需要結(jié)合實(shí)際情況從多方面考慮是否改變,否則一味改變反而會(huì)得不償失。
3.4 理解垃圾回收機(jī)制 3.4.1 垃圾回收簡(jiǎn)介 垃圾回收(英語(yǔ):Garbage Collection,縮寫為 GC ) ,顧名思義,是一種自動(dòng)的內(nèi)存管理機(jī)制。
當(dāng)一個(gè)電腦上的動(dòng)態(tài)內(nèi)存不再需要時(shí),就應(yīng)該予以釋放,以讓出內(nèi)存,這種內(nèi)存資源管理的機(jī)制,稱為垃圾回收
。垃圾回收機(jī)制可以讓程序員不必過(guò)分關(guān)心程序內(nèi)存分配,從而將更多的精力投入到業(yè)務(wù)邏輯。
機(jī)制升級(jí)
在 PHP5.2 及更早版本的 PHP 中,引擎在判斷一個(gè)變量空間是否能夠被釋放是依據(jù)這個(gè)變量的 zval 的 refcount 的是否為 0,如果為 0 變量的空間可以被釋放,否則就不釋,這是一種簡(jiǎn)單的 GC 實(shí)現(xiàn)方案。在這種簡(jiǎn)單的 GC 實(shí)現(xiàn)方案中,出現(xiàn)了意想不到的變量?jī)?nèi)存泄漏情況,引擎無(wú)法回收這些內(nèi)存。于是在 PHP5.3 中出現(xiàn)了新的 GC,新的 GC 有專門的機(jī)制負(fù)責(zé)清理垃圾數(shù)據(jù),防止內(nèi)存泄漏。 3.4.2 變量簡(jiǎn)介 在繼續(xù)了解垃圾回收機(jī)制之前,我們需要先知道一個(gè)前提知識(shí)點(diǎn) - 變量。 PHP 中變量存在于一個(gè) zval 的變量容器中,此結(jié)構(gòu)包含了變量的類型和值信息,這兩個(gè)是眾所周知的。另外還有兩個(gè)字段信息 - is_ref 與 refcount。 is_ref 字段主要作用是用來(lái)標(biāo)識(shí)變量是否是一個(gè)引用,通過(guò)該字段 PHP 可以區(qū)分一般變量和引用變量。refcount 字段表示有多少個(gè)變量指向這個(gè) zval 容器,當(dāng)該字段為 0 則說(shuō)明沒有任何變量指向該字段,就可以被釋放。 例 : # 1.當(dāng)創(chuàng)建一個(gè)變量 a $a = 'hello world'; ## 對(duì)應(yīng)的信息為 a: (refcount=1, is_ref=0) ? # 賦值 # 2.創(chuàng)建變量 b,其值為 a 變量 $b = $a; ## 對(duì)應(yīng)的信息為 a: (refcount=2, is_ref=0) !!! 這里需要注意的是此時(shí) a 和 b 兩個(gè)變量指向同一個(gè) zval, 只有當(dāng)原變量發(fā)生改變時(shí), 才會(huì)為新變量分配內(nèi)存空間, 同時(shí)原變量的 refcount 減 1。當(dāng)然, 如果 unset 原變量, 新變量直接就使用原變量的 zval 而不是重新分配。 ? # 引用 # 3.創(chuàng)建變量 c, 其值為 a 的引用賦值 $c = &$a; ## 此時(shí)對(duì)應(yīng)的信息為 a:(refcount=2, is_ref=1) 通過(guò)上述例子,我們對(duì) refcount 與 is_ref 字段有了一些了解了。但是上述主要是以標(biāo)量為例,復(fù)合類型會(huì)相對(duì)麻煩。 安裝 xdebug 拓展后,可以利用 xdebug_debug_zval 打印出 zval 容器信息。
3.4.3 復(fù)合類型 廢話不多說(shuō),我們直接創(chuàng)建一個(gè)數(shù)組來(lái)查看 : $a = array( 'meaning' => 'life', 'number' => 42 ); ## 對(duì)應(yīng)信息為: a: (refcount=1, is_ref=0)=array ( ?'meaning' => (refcount=1, is_ref=0)='life', ?'number' => (refcount=1, is_ref=0)=42 ) 圖示為 :
這是正常情況下所創(chuàng)建的一個(gè)數(shù)組,接下來(lái)看一個(gè)不正常的 : # 在創(chuàng)建一個(gè)數(shù)組后,我們直接使用該 $a = array( 'one' ); $a[] =& $a; ## 對(duì)應(yīng)信息為: a: (refcount=2, is_ref=1)=array ( ?0 => (refcount=1, is_ref=0)='one', ?1 => (refcount=2, is_ref=1)=...?# 這里的 ... 代表發(fā)生了遞歸操作,而這里則是指向數(shù)組本身 ) 圖示為 :
此時(shí)我們刪除一個(gè)變量將刪除這個(gè)符號(hào),且 refcount 也會(huì)減 1。結(jié)果為 : # 使用 unset 刪除 $a (refcount=1, is_ref=1)=array ( ?0 => (refcount=1, is_ref=0)='one', ?1 => (refcount=1, is_ref=1)=... ) 對(duì)應(yīng)圖示為 :
此時(shí)用戶不能再使用 $a 了,但是 refcount 的值沒有歸零,所以 5.2 及更早版本的 PHP 不會(huì)自動(dòng)釋放這一部分,這一部分即為所謂的 ‘垃圾’。在請(qǐng)求結(jié)束的時(shí)候倒是會(huì)清除,但是在清除之前會(huì)消耗不少內(nèi)存。出現(xiàn)的情況較少還好,當(dāng)情況出現(xiàn)的過(guò)多時(shí),就會(huì)容易導(dǎo)致內(nèi)存不足而崩潰。
垃圾開關(guān)
由于垃圾回收算法做了額外的事情,所以這也是相應(yīng)的會(huì)消耗一些時(shí)間,垃圾回收也是一種時(shí)間換空間的機(jī)制。但 PHP 比較人性化的給我們提供了開關(guān),可以根據(jù)自己的需求設(shè)置 : 通過(guò) ini 文件中的 zend.enable_gc 項(xiàng)來(lái)開啟或則關(guān)閉 GC(PHP 默認(rèn)開啟)。
在程序中使用 gc_enable() 和 gc_disable() 開啟和關(guān)閉。
在 PHP 手冊(cè)「性能方面考慮的因素」一文中對(duì)性能方面做了測(cè)試。開啟垃圾回收時(shí),消耗的時(shí)間百分比是 7%,而節(jié)省的內(nèi)存則是 98%(數(shù)據(jù)僅供參考)。以上可以根據(jù)自己的需求來(lái)決定自己的設(shè)置。 3.4.4 垃圾回收機(jī)制 在上一部分中,我們通過(guò)一個(gè)案例了解到了什么是 ‘垃圾’。PHP 5.3 及之后版本采用了比較復(fù)雜的算法來(lái)處理環(huán)狀引用導(dǎo)致內(nèi)存泄露的問(wèn)題(此處不深究算法)。當(dāng)一個(gè) zval 可能為垃圾時(shí),回收算法會(huì)把該 zval 放入一個(gè)內(nèi)存緩沖區(qū)。當(dāng)緩沖區(qū)達(dá)到最大臨界值時(shí),回收算法會(huì)循環(huán)遍歷所有緩沖區(qū)中的 zval,判斷其是否為垃圾,如果是則進(jìn)行釋放處理。 在 PHP5.3 的 GC 中,針對(duì)的垃圾做了如下準(zhǔn)則 : 如果一個(gè) zval 的 refcount 增加,那么此 zval 還在使用,判定不是垃圾,不會(huì)進(jìn)入緩沖區(qū)。
如果一個(gè) zval 的 refcount 減少到 0, 那么 zval 會(huì)被立即釋放掉,不屬于 GC 要處理的垃圾對(duì)象,不會(huì)進(jìn)入緩沖區(qū)。
如果一個(gè) zval 的 refcount 減少之后大于 0,該 zval 還不能被釋放,那么該 zval 可能成為一個(gè)垃圾,將被放入緩沖區(qū)。
只有在準(zhǔn)則 3 下,GC 才會(huì)把 zval 收集起來(lái),然后通過(guò)新的算法來(lái)判斷此 zval 是否為垃圾。