摘要:這種行為比最初出現(xiàn)的問題更為棘手,同時(shí)也是一種常見的錯(cuò)誤源。這意味著這個(gè)數(shù)組的一份拷貝將會(huì)被返回,因此被調(diào)函數(shù)與調(diào)用者所訪問的數(shù)組并不是同樣的數(shù)組實(shí)例。
PHP 語言讓 WEB 端程序設(shè)計(jì)變得簡單,這也是它能流行起來的原因。但也是因?yàn)樗暮唵?,PHP 也慢慢發(fā)展成一個(gè)相對(duì)復(fù)雜的語言,層出不窮的框架,各種語言特性和版本差異都時(shí)常讓搞的我們頭大,不得不浪費(fèi)大量時(shí)間去調(diào)試。這篇文章列出了十個(gè)最容易出錯(cuò)的地方,值得我們?nèi)プ⒁狻?/p> 易犯錯(cuò)誤 #1: 在 foreach循環(huán)后留下數(shù)組的引用
還不清楚 PHP 中 foreach 遍歷的工作原理?如果你在想遍歷數(shù)組時(shí)操作數(shù)組中每個(gè)元素,在 foreach 循環(huán)中使用引用會(huì)十分方便,例如
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr 現(xiàn)在是 array(2, 4, 6, 8)
問題是,如果你不注意的話這會(huì)導(dǎo)致一些意想不到的負(fù)面作用。在上述例子,在代碼執(zhí)行完以后,$value 仍保留在作用域內(nèi),并保留著對(duì)數(shù)組最后一個(gè)元素的引用。之后與 $value 相關(guān)的操作會(huì)無意中修改數(shù)組中最后一個(gè)元素的值。
你要記住 foreach 并不會(huì)產(chǎn)生一個(gè)塊級(jí)作用域。因此,在上面例子中 $value 是一個(gè)全局引用變量。在 foreach 遍歷中,每一次迭代都會(huì)形成一個(gè)對(duì) $arr 下一個(gè)元素的引用。當(dāng)遍歷結(jié)束后, $value 會(huì)引用 $arr 的最后一個(gè)元素,并保留在作用域中
這種行為會(huì)導(dǎo)致一些不易發(fā)現(xiàn)的,令人困惑的bug,以下是一個(gè)例子
$array = [1, 2, 3]; echo implode(",", $array), " "; foreach ($array as &$value) {} // 通過引用遍歷 echo implode(",", $array), " "; foreach ($array as $value) {} // 通過賦值遍歷 echo implode(",", $array), " ";
以上代碼會(huì)輸出
1,2,3 1,2,3 1,2,2
你沒有看錯(cuò),最后一行的最后一個(gè)值是 2 ,而不是 3 ,為什么?
在完成第一個(gè) foreach 遍歷后, $array 并沒有改變,但是像上述解釋的那樣, $value 留下了一個(gè)對(duì) $array 最后一個(gè)元素的危險(xiǎn)的引用(因?yàn)?foreach 通過引用獲得 $value )
這導(dǎo)致當(dāng)運(yùn)行到第二個(gè) foreach ,這個(gè)"奇怪的東西"發(fā)生了。當(dāng) $value 通過賦值獲得, foreach 按順序復(fù)制每個(gè) $array 的元素到 $value 時(shí),第二個(gè) foreach 里面的細(xì)節(jié)是這樣的
第一步:復(fù)制 $array[0] (也就是 1 )到 $value ($value 其實(shí)是 $array最后一個(gè)元素的引用,即 $array[2]),所以 $array[2] 現(xiàn)在等于 1。所以 $array 現(xiàn)在包含 [1, 2, 1]
第二步:復(fù)制 $array[1](也就是 2 )到 $value ( $array[2] 的引用),所以 $array[2] 現(xiàn)在等于 2。所以 $array 現(xiàn)在包含 [1, 2, 2]
第三步:復(fù)制 $array[2](現(xiàn)在等于 2 ) 到 $value ( $array[2] 的引用),所以 $array[2] 現(xiàn)在等于 2 。所以 $array 現(xiàn)在包含 [1, 2, 2]
為了在 foreach 中方便的使用引用而免遭這種麻煩,請(qǐng)?jiān)?foreach 執(zhí)行完畢后 unset() 掉這個(gè)保留著引用的變量。例如
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value 不再引用 $arr[3]常見錯(cuò)誤 #2: 誤解?isset()?的行為
盡管名字叫 isset,但是 isset()?不僅會(huì)在變量不存在的時(shí)候返回 false,在變量值為 null 的時(shí)候也會(huì)返回 false。
這種行為比最初出現(xiàn)的問題更為棘手,同時(shí)也是一種常見的錯(cuò)誤源。
看看下面的代碼:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data["keyShouldBeSet"]) { // do something here if "keyShouldBeSet" is not set }
開發(fā)者想必是想確認(rèn)?keyShouldBeSet?是否存在于?$data 中。然而,正如上面說的,如果?$data["keyShouldBeSet"] 存在并且值為 null 的時(shí)候,?isset($data["keyShouldBeSet"])?也會(huì)返回 false。所以上面的邏輯是不嚴(yán)謹(jǐn)?shù)摹?/p>
我們來看另外一個(gè)例子:
if ($_POST["active"]) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo "post not active"; }
上述代碼,通常認(rèn)為,假如?$_POST["active"]?返回?true,那么?postData?必將存在,因此?isset($postData)?也將返回?true。反之,?isset($postData) 返回?false?的唯一可能是?$_POST["active"]?也返回?false。
然而事實(shí)并非如此!
如我所言,如果$postData?存在且被設(shè)置為?null, isset($postData)?也會(huì)返回?false?。 也就是說,即使?$_POST["active"]?返回?true,?isset($postData)?也可能會(huì)返回?false?。 再一次說明上面的邏輯不嚴(yán)謹(jǐn)。
順便一提,如果上面代碼的意圖真的是再次確認(rèn)?$_POST["active"] 是否返回 true,依賴?isset()?來做,不管對(duì)于哪種場景來說都是一種糟糕的決定。更好的做法是再次檢查 $_POST["active"],即:
if ($_POST["active"]) { $postData = extractSomething($_POST); } // ... if ($_POST["active"]) { echo "post not active"; }
對(duì)于這種情況,雖然檢查一個(gè)變量是否真的存在很重要(即:區(qū)分一個(gè)變量是未被設(shè)置還是被設(shè)置為?null);但是使用 array_key_exists()?這個(gè)函數(shù)卻是個(gè)更健壯的解決途徑。
比如,我們可以像下面這樣重寫上面第一個(gè)例子:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists("keyShouldBeSet", $data)) { // do this if "keyShouldBeSet" isn"t set }
另外,通過結(jié)合 array_key_exists() 和?get_defined_vars(), 我們能更加可靠的判斷一個(gè)變量在當(dāng)前作用域中是否存在:
if (array_key_exists("varShouldBeSet", get_defined_vars())) { // variable $varShouldBeSet exists in current scope }常見錯(cuò)誤 #3:關(guān)于通過引用返回與通過值返回的困惑
考慮下面的代碼片段:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()["test"] = "test"; echo $config->getValues()["test"];
如果你運(yùn)行上面的代碼,將得到下面的輸出:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
出了什么問題?
上面代碼的問題在于沒有搞清楚通過引用與通過值返回?cái)?shù)組的區(qū)別。除非你明確告訴 PHP 通過引用返回一個(gè)數(shù)組(例如,使用 &),否則 PHP 默認(rèn)將會(huì)「通過值」返回這個(gè)數(shù)組。這意味著這個(gè)數(shù)組的一份拷貝將會(huì)被返回,因此被調(diào)函數(shù)與調(diào)用者所訪問的數(shù)組并不是同樣的數(shù)組實(shí)例。
所以上面對(duì) getValues() 的調(diào)用將會(huì)返回 $values 數(shù)組的一份拷貝,而不是對(duì)它的引用??紤]到這一點(diǎn),讓我們重新回顧一下以上例子中的兩個(gè)關(guān)鍵行:
// getValues() 返回了一個(gè) $values 數(shù)組的拷貝 // 所以`test`元素被添加到了這個(gè)拷貝中,而不是 $values 數(shù)組本身。 $config->getValues()["test"] = "test"; // getValues() 又返回了另一份 $values 數(shù)組的拷貝 // 且這份拷貝中并不包含一個(gè)`test`元素(這就是為什么我們會(huì)得到 「未定義索引」 消息)。 echo $config->getValues()["test"];
一個(gè)可能的修改方法是存儲(chǔ)第一次通過 getValues() 返回的 $values 數(shù)組拷貝,然后后續(xù)操作都在那份拷貝上進(jìn)行;例如:
$vals = $config->getValues(); $vals["test"] = "test"; echo $vals["test"];
這段代碼將會(huì)正常工作(例如,它將會(huì)輸出test而不會(huì)產(chǎn)生任何「未定義索引」消息),但是這個(gè)方法可能并不能滿足你的需求。特別是上面的代碼并不會(huì)修改原始的$values數(shù)組。如果你想要修改原始的數(shù)組(例如添加一個(gè)test元素),就需要修改getValues()函數(shù),讓它返回一個(gè)$values數(shù)組自身的引用。通過在函數(shù)名前面添加一個(gè)&來說明這個(gè)函數(shù)將返回一個(gè)引用;例如:
class Config { private $values = []; // 返回一個(gè) $values 數(shù)組的引用 public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()["test"] = "test"; echo $config->getValues()["test"];
這會(huì)輸出期待的test。
但是現(xiàn)在讓事情更困惑一些,請(qǐng)考慮下面的代碼片段:
class Config { private $values; // 使用數(shù)組對(duì)象而不是數(shù)組 public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()["test"] = "test"; echo $config->getValues()["test"];
如果你認(rèn)為這段代碼會(huì)導(dǎo)致與之前的數(shù)組例子一樣的「未定義索引」錯(cuò)誤,那就錯(cuò)了。實(shí)際上,這段代碼將會(huì)正常運(yùn)行。原因是,與數(shù)組不同,PHP 永遠(yuǎn)會(huì)將對(duì)象按引用傳遞。(ArrayObject 是一個(gè) SPL 對(duì)象,它完全模仿數(shù)組的用法,但是卻是以對(duì)象來工作。)
像以上例子說明的,你應(yīng)該以引用還是拷貝來處理通常不是很明顯就能看出來。因此,理解這些默認(rèn)的行為(例如,變量和數(shù)組以值傳遞;對(duì)象以引用傳遞)并且仔細(xì)查看你將要調(diào)用的函數(shù) API 文檔,看看它是返回一個(gè)值,數(shù)組的拷貝,數(shù)組的引用或是對(duì)象的引用是必要的。
盡管如此,我們要認(rèn)識(shí)到應(yīng)該盡量避免返回一個(gè)數(shù)組或 ArrayObject,因?yàn)檫@會(huì)讓調(diào)用者能夠修改實(shí)例對(duì)象的私有數(shù)據(jù)。這就破壞了對(duì)象的封裝性。所以最好的方式是使用傳統(tǒng)的「getters」和「setters」,例如:
class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue("testKey", "testValue"); echo $config->getValue("testKey"); // 輸出『testValue』
這個(gè)方法讓調(diào)用者可以在不對(duì)私有的$values數(shù)組本身進(jìn)行公開訪問的情況下設(shè)置或者獲取數(shù)組中的任意值。
常見的錯(cuò)誤 #4:在循環(huán)中執(zhí)行查詢如果像這樣的話,一定不難見到你的 PHP 無法正常工作。
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
這里也許沒有真正的錯(cuò)誤, 但是如果你跟隨著代碼的邏輯走下去, 你也許會(huì)發(fā)現(xiàn)這個(gè)看似無害的調(diào)用$valueRepository->findByValue()?最終執(zhí)行了這樣一種查詢,例如:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
結(jié)果每輪循環(huán)都會(huì)產(chǎn)生一次對(duì)數(shù)據(jù)庫的查詢。 因此,假如你為這個(gè)循環(huán)提供了一個(gè)包含 1000 個(gè)值的數(shù)組,它會(huì)對(duì)資源產(chǎn)生 1000 多帶帶的請(qǐng)求!如果這樣的腳本在多個(gè)線程中被調(diào)用,他會(huì)有導(dǎo)致系統(tǒng)崩潰的潛在危險(xiǎn)。
因此,至關(guān)重要的是,當(dāng)你的代碼要進(jìn)行查詢時(shí),應(yīng)該盡可能的收集需要用到的值,然后在一個(gè)查詢中獲取所有結(jié)果。
一個(gè)我們平時(shí)常常能見到查詢效率低下的地方 (例如:在循環(huán)中)是使用一個(gè)數(shù)組中的值 (比如說很多的 ID )向表發(fā)起請(qǐng)求。檢索每一個(gè) ID 的所有的數(shù)據(jù),代碼將會(huì)迭代這個(gè)數(shù)組,每個(gè) ID 進(jìn)行一次SQL查詢請(qǐng)求,它看起來常常是這樣:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
但是 只用一條 SQL 查詢語句就可以更高效的完成相同的工作,比如像下面這樣:
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(",", $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
因此在你的代碼直接或間接進(jìn)行查詢請(qǐng)求時(shí),一定要認(rèn)出這種查詢。盡可能的通過一次查詢得到想要的結(jié)果。然而,依然要小心謹(jǐn)慎,不然就可能會(huì)出現(xiàn)下面我們要講的另一個(gè)易犯的錯(cuò)誤...
常見問題 #5: 內(nèi)存使用欺騙與低效一次取多條記錄肯定是比一條條的取高效,但是當(dāng)我們使用 PHP 的?mysql?擴(kuò)展的時(shí)候,這也可能成為一個(gè)導(dǎo)致?libmysqlclient?出現(xiàn)『內(nèi)存不足』(out of memory)的條件。
我們?cè)谝粋€(gè)測試盒里演示一下,該測試盒的環(huán)境是:有限的內(nèi)存(512MB RAM),MySQL,和 php-cli。
我們將像下面這樣引導(dǎo)一個(gè)數(shù)據(jù)表:
// 連接 mysql $connection = new mysqli("localhost", "username", "password", "database"); // 創(chuàng)建 400 個(gè)字段 $query = "CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT"; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ");"; $connection->query($query); // 寫入 2 百萬行數(shù)據(jù) for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ", " . mt_rand(1000000000, 9999999999); } $query .= ")"; $connection->query($query); }
OK,現(xiàn)在讓我們一起來看一下內(nèi)存使用情況:
// 連接 mysql $connection = new mysqli("localhost", "username", "password", "database"); echo "Before: " . memory_get_peak_usage() . " "; $res = $connection->query("SELECT `x`,`y` FROM `test` LIMIT 1"); echo "Limit 1: " . memory_get_peak_usage() . " "; $res = $connection->query("SELECT `x`,`y` FROM `test` LIMIT 10000"); echo "Limit 10000: " . memory_get_peak_usage() . " ";
輸出結(jié)果是:
Before: 224704 Limit 1: 224704 Limit 10000: 224704
Cool。 看來就內(nèi)存使用而言,內(nèi)部安全地管理了這個(gè)查詢的內(nèi)存。
為了更加明確這一點(diǎn),我們把限制提高一倍,使其達(dá)到 100,000。 額~如果真這么干了,我們將會(huì)得到如下結(jié)果:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
究竟發(fā)生了啥?
這就涉及到 PHP 的?mysql 模塊的工作方式的問題了。它其實(shí)只是個(gè) libmysqlclient 的代理,專門負(fù)責(zé)干臟活累活。每查出一部分?jǐn)?shù)據(jù)后,它就立即把數(shù)據(jù)放入內(nèi)存中。由于這塊內(nèi)存還沒被 PHP 管理,所以,當(dāng)我們?cè)诓樵兝镌黾酉拗频臄?shù)量的時(shí)候, memory_get_peak_usage()?不會(huì)顯示任何增加的資源使用情況?。我們被『內(nèi)存管理沒問題』這種自滿的思想所欺騙了,所以才會(huì)導(dǎo)致上面的演示出現(xiàn)那種問題。 老實(shí)說,我們的內(nèi)存管理確實(shí)是有缺陷的,并且我們也會(huì)遇到如上所示的問題。
如果使用?mysqlnd?模塊的話,你至少可以避免上面那種欺騙(盡管它自身并不會(huì)提升你的內(nèi)存利用率)。?mysqlnd 被編譯成原生的 PHP 擴(kuò)展,并且確實(shí) 會(huì) 使用 PHP 的內(nèi)存管理器。
因此,如果使用?mysqlnd?而不是?mysql,我們將會(huì)得到更真實(shí)的內(nèi)存利用率的信息:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
順便一提,這比剛才更糟糕。根據(jù) PHP 的文檔所說,mysql?使用?mysqlnd?兩倍的內(nèi)存來存儲(chǔ)數(shù)據(jù), 所以,原來使用?mysql?那個(gè)腳本真正使用的內(nèi)存比這里顯示的更多(大約是兩倍)。
為了避免出現(xiàn)這種問題,考慮限制一下你查詢的數(shù)量,使用一個(gè)較小的數(shù)字來循環(huán),像這樣:
$totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }
當(dāng)我們把這個(gè)常見錯(cuò)誤和上面的?常見錯(cuò)誤 #4?結(jié)合起來考慮的時(shí)候, 就會(huì)意識(shí)到我們的代碼理想需要在兩者間實(shí)現(xiàn)一個(gè)平衡。是讓查詢粒度化和重復(fù)化,還是讓單個(gè)查詢巨大化。生活亦是如此,平衡不可或缺;哪一個(gè)極端都不好,都可能會(huì)導(dǎo)致 PHP 無法正常運(yùn)行。
常見錯(cuò)誤 #6: 忽略 Unicode/UTF-8 的問題從某種意義上說,這實(shí)際上是PHP本身的一個(gè)問題,而不是你在調(diào)試 PHP 時(shí)遇到的問題,但是它從未得到妥善的解決。 PHP 6 的核心就是要做到支持 Unicode。但是隨著 PHP 6 在 2010 年的暫停而擱置了。
這并不意味著開發(fā)者能夠避免?正確處理 UTF-8?并避免做出所有字符串必須是『古老的 ASCII』的假設(shè)。 沒有正確處理非 ASCII 字符串的代碼會(huì)因?yàn)橐氪植诘?海森堡bug(heisenbugs) ?而變得臭名昭著。當(dāng)一個(gè)名字包含 『Schr?dinger』的人注冊(cè)到你的系統(tǒng)時(shí),即使簡單的?strlen($_POST["name"]) 調(diào)用也會(huì)出現(xiàn)問題。
下面是一些可以避免出現(xiàn)這種問題的清單:
如果你對(duì) UTF-8 還不了解,那么你至少應(yīng)該了解下基礎(chǔ)的東西。?這兒 有個(gè)很好的引子。
確保使用?mb_*?函數(shù)代替老舊的字符串處理函數(shù)(需要先保證你的 PHP 構(gòu)建版本開啟了『多字節(jié)』(multibyte)擴(kuò)展)。
確保你的數(shù)據(jù)庫和表設(shè)置了 Unicode 編碼(許多 MySQL 的構(gòu)建版本仍然默認(rèn)使用?latin1? )。
記住?json_encode()?會(huì)轉(zhuǎn)換非 ASCII 標(biāo)識(shí)(比如: 『Schr?dinger』會(huì)被轉(zhuǎn)換成 『Schru00f6dinger』),但是 serialize()?不會(huì) 轉(zhuǎn)換。
確保 PHP 文件也是 UTF-8 編碼,以避免在連接硬編碼字符串或者配置字符串常量的時(shí)候產(chǎn)生沖突。
Francisco Claria? 在本博客上發(fā)表的 UTF-8 Primer for PHP and MySQL??是份寶貴的資源。
常見錯(cuò)誤 #7: 認(rèn)為 $_POST 總是包含你 POST 的數(shù)據(jù)不管它的名稱,$_POST 數(shù)組不是總是包含你 POST 的數(shù)據(jù),他也有可能會(huì)是空的。 為了理解這一點(diǎn),讓我們來看一下下面這個(gè)例子。假設(shè)我們使用 jQuery.ajax() 模擬一個(gè)服務(wù)請(qǐng)求,如下:
// js $.ajax({ url: "http://my.site/some/path", method: "post", data: JSON.stringify({a: "a", b: "b"}), contentType: "application/json" });
(順帶一提,注意這里的 contentType: "application/json" 。我們用 JSON 類型發(fā)送數(shù)據(jù),這在接口中非常流行。這在 AngularJS $http service 里是默認(rèn)的發(fā)送數(shù)據(jù)的類型。)
在我們舉例子的服務(wù)端,我們簡單的打印一下?$_POST 數(shù)組:
// php var_dump($_POST);
奇怪的是,結(jié)果如下:
array(0) { }
為什么?我們的 JSON 串 {a: "a", b: "b"} 究竟發(fā)生了什么?
原因在于?當(dāng)內(nèi)容類型為 application/x-www-form-urlencoded 或者 multipart/form-data 的時(shí)候 PHP 只會(huì)自動(dòng)解析一個(gè) POST 的有效內(nèi)容。這里面有歷史的原因 --- 這兩種內(nèi)容類型是在 PHP 的 $_POST 實(shí)現(xiàn)前就已經(jīng)在使用了的兩個(gè)重要的類型。所以不管使用其他任何內(nèi)容類型 (即使是那些現(xiàn)在很流行的,像?application/json), PHP 也不會(huì)自動(dòng)加載到 POST 的有效內(nèi)容。
既然 $_POST 是一個(gè)超級(jí)全局變量,如果我們重寫 一次 (在我們的腳本里盡可能早的),被修改的值(包括 POST 的有效內(nèi)容)將可以在我們的代碼里被引用。這很重要因?yàn)?$_POST 已經(jīng)被 PHP 框架和幾乎所有的自定義的腳本普遍使用來獲取和傳遞請(qǐng)求數(shù)據(jù)。
所以,舉個(gè)例子,當(dāng)處理一個(gè)內(nèi)容類型為?application/json 的 POST 有效內(nèi)容的時(shí)候 ,我們需要手動(dòng)解析請(qǐng)求內(nèi)容(decode 出 JSON 數(shù)據(jù))并且覆蓋 $_POST 變量,如下:
// php $_POST = json_decode(file_get_contents("php://input"), true);
然后當(dāng)我們打印 $_POST 數(shù)組的時(shí)候,我們可以看到他正確的包含了 POST 的有效內(nèi)容;如下:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }常見錯(cuò)誤 #8: 認(rèn)為 PHP 支持單字符數(shù)據(jù)類型
閱讀下面的代碼并思考會(huì)輸出什么:
for ($c = "a"; $c <= "z"; $c++) { echo $c . " "; }
如果你的答案是 a 到 z,那么你可能會(huì)對(duì)這是一個(gè)錯(cuò)誤答案感到吃驚。
沒錯(cuò),它確實(shí)會(huì)輸出 a 到 z,但是,它還會(huì)繼續(xù)輸出 aa 到 yz。我們一起來看一下這是為什么。
PHP 中沒有?char?數(shù)據(jù)類型; 只能用?string?類型。記住一點(diǎn),在 PHP 中增加?string?類型的 z?得到的是?aa:
php> $c = "z"; echo ++$c . " "; aa
沒那么令人混淆的是,aa 的字典順序是?小于 ?z 的:
php> var_export((boolean)("aa" < "z")) . " "; true
這也是為什么上面那段簡單的代碼會(huì)輸出?a?到?z, 然后 繼續(xù)?輸出 aa到?yz。 它停在了?za,那是它遇到的第一個(gè)比?z 大 的:
php> var_export((boolean)("za" < "z")) . " "; false
事實(shí)上,在 PHP 里 有合適的 方式在循環(huán)中輸出 a 到 z 的值:
for ($i = ord("a"); $i <= ord("z"); $i++) { echo chr($i) . " "; }
或者是這樣:
$letters = range("a", "z"); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . " "; }常見 錯(cuò)誤 #9: 忽視代碼規(guī)范
盡管忽視代碼標(biāo)準(zhǔn)并不直接導(dǎo)致需要去調(diào)試 PHP 代碼,但這可能是所有需要談?wù)摰氖虑槔镒钪匾囊豁?xiàng)。
在一個(gè)項(xiàng)目中忽視代碼規(guī)范能夠?qū)е麓罅康膯栴}。最樂觀的預(yù)計(jì),前后代碼不一致(在此之前每個(gè)開發(fā)者都在“做自己的事情”)。但最差的結(jié)果,PHP 代碼不能運(yùn)行或者很難(有時(shí)是不可能的)去順利通過,這對(duì)于 調(diào)試代碼、提升性能、維護(hù)項(xiàng)目來說也是困難重重。并且這意味著降低你們團(tuán)隊(duì)的生產(chǎn)力,增加大量的額外(或者至少是本不必要的)精力消耗。
幸運(yùn)的是對(duì)于 PHP 開發(fā)者來說,存在 PHP 編碼標(biāo)準(zhǔn)建議(PSR),它由下面的五個(gè)標(biāo)準(zhǔn)組成:
PSR-0: 自動(dòng)加載標(biāo)準(zhǔn)
PSR-1: 基礎(chǔ)編碼標(biāo)準(zhǔn)
PSR-2: 編碼風(fēng)格指導(dǎo)
PSR-3: 日志接口
PSR-4: 自動(dòng)加載增強(qiáng)版
PSR 起初是由市場上最大的組織平臺(tái)維護(hù)者創(chuàng)造的。 Zend, Drupal, Symfony, Joomla 和?其他?為這些標(biāo)準(zhǔn)做出了貢獻(xiàn),并一直遵守它們。甚至,多年前試圖成為一個(gè)標(biāo)準(zhǔn)的 PEAR ,現(xiàn)在也加入到 PSR 中來。
某種意義上,你的代碼標(biāo)準(zhǔn)是什么幾乎是不重要的,只要你遵循一個(gè)標(biāo)準(zhǔn)并堅(jiān)持下去,但一般來講,跟隨 PSR 是一個(gè)很不錯(cuò)的主意,除非你的項(xiàng)目上有其他讓人難以抗拒的理由。越來越多的團(tuán)隊(duì)和項(xiàng)目正在遵從 PSR 。在這一點(diǎn)上,大部分的 PHP 開發(fā)者達(dá)成了共識(shí),因此使用 PSR 代碼標(biāo)準(zhǔn),有利于使新加入團(tuán)隊(duì)的開發(fā)者對(duì)你的代碼標(biāo)準(zhǔn)感到更加的熟悉與舒適。
常見錯(cuò)誤 #10: ?濫用 empty()一些 PHP 開發(fā)者喜歡對(duì)幾乎所有的事情使用?empty()?做布爾值檢驗(yàn)。不過,在一些情況下,這會(huì)導(dǎo)致混亂。
首先,讓我們回到數(shù)組和?ArrayObject?實(shí)例(和數(shù)組類似)??紤]到他們的相似性,很容易假設(shè)它們的行為是相同的。然而,事實(shí)證明這是一個(gè)危險(xiǎn)的假設(shè)。舉例,在 PHP 5.0 中:
// PHP 5.0 或后續(xù)版本: $array = []; var_dump(empty($array)); // 輸出 bool(true) $array = new ArrayObject(); var_dump(empty($array)); // 輸出 bool(false) // 為什么這兩種方法不產(chǎn)生相同的輸出呢?
更糟糕的是,PHP 5.0之前的結(jié)果可能是不同的:
// PHP 5.0 之前: $array = []; var_dump(empty($array)); // 輸出 bool(false) $array = new ArrayObject(); var_dump(empty($array)); // 輸出 bool(false)
這種方法上的不幸是十分普遍的。比如,在 Zend Framework 2 下的 ZendDbTableGateway?的 TableGateway::select() 結(jié)果中調(diào)用?current() 時(shí)返回?cái)?shù)據(jù)的方式,正如文檔所表明的那樣。開發(fā)者很容易就會(huì)變成此類數(shù)據(jù)錯(cuò)誤的受害者。
為了避免這些問題的產(chǎn)生,更好的方法是使用 count() 去檢驗(yàn)空數(shù)組結(jié)構(gòu):
// 注意這會(huì)在 PHP 的所有版本中發(fā)揮作用 (5.0 前后都是): $array = []; var_dump(count($array)); // 輸出 int(0) $array = new ArrayObject(); var_dump(count($array)); // 輸出 int(0)
順便說一句, 由于 PHP 將 0 轉(zhuǎn)換為 false , count() 能夠被使用在 if() 條件內(nèi)部去檢驗(yàn)空數(shù)組。同樣值得注意的是,在 PHP 中, count() 在數(shù)組中是常量復(fù)雜度 (O(1) 操作) ,這更清晰的表明它是正確的選擇。
另一個(gè)使用 empty() 產(chǎn)生危險(xiǎn)的例子是當(dāng)它和魔術(shù)方法 _get() 一起使用。我們來定義兩個(gè)類并使其都有一個(gè) test 屬性。
首先我們定義包含 test 公共屬性的?Regular?類。
class Regular { public $test = "value"; }
然后我們定義?Magic?類,這里使用魔術(shù)方法?__get()?來操作去訪問它的?test?屬性:
class Magic { private $values = ["test" => "value"]; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
好了,現(xiàn)在我們嘗試去訪問每個(gè)類中的 test 屬性看看會(huì)發(fā)生什么:
$regular = new Regular(); var_dump($regular->test); // 輸出 string(4) "value" $magic = new Magic(); var_dump($magic->test); // 輸出 string(4) "value"
到目前為止還好。
但是現(xiàn)在當(dāng)我們對(duì)其中的每一個(gè)都調(diào)用 empty() ,讓我們看看會(huì)發(fā)生什么:
var_dump(empty($regular->test)); // 輸出 bool(false) var_dump(empty($magic->test)); // 輸出 bool(true)
咳。所以如果我們依賴 empty() ,我們很可能誤認(rèn)為 $magic 的屬性 test 是空的,而實(shí)際上它被設(shè)置為 "value"。
不幸的是,如果類使用魔術(shù)方法 __get() 來獲取屬性值,那么就沒有萬無一失的方法來檢查該屬性值是否為空。
在類的作用域之外,你僅僅只能檢查是否將返回一個(gè) null 值,這并不意味著沒有設(shè)置相應(yīng)的鍵,因?yàn)樗鼘?shí)際上還可能被設(shè)置為 null 。
相反,如果我們?cè)噲D去引用 Regular 類實(shí)例中不存在的屬性,我們將得到一個(gè)類似于以下內(nèi)容的通知:
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0
所以這里的主要觀點(diǎn)是 empty() 方法應(yīng)該被謹(jǐn)慎地使用,因?yàn)槿绻恍⌒牡脑捤赡軐?dǎo)致混亂 -- 甚至潛在的誤導(dǎo) -- 結(jié)果。
總結(jié)PHP 的易用性讓開發(fā)者陷入一種虛假的舒適感,語言本身的一些細(xì)微差別和特質(zhì),可能花費(fèi)掉你大量的時(shí)間去調(diào)試。這些可能會(huì)導(dǎo)致 PHP 程序無法正常工作,并導(dǎo)致諸如此處所述的問題。
PHP 在其20年的歷史中,已經(jīng)發(fā)生了顯著的變化?;〞r(shí)間去熟悉語言本身的微妙之處是值得的,因?yàn)樗兄诖_保你編寫的軟件更具可擴(kuò)展性,健壯和可維護(hù)性。
更多現(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/28513.html
摘要:大刀闊斧的改造在學(xué)習(xí)了兩遍之后,基于教程開發(fā)的校園二手書交易平臺(tái)熊能本周閱讀清單紙牌屋弗蘭克知道的太晚了實(shí)現(xiàn)微信紅包拆分算法聊聊最近求職發(fā)生的故事無銘更多現(xiàn)代化知識(shí),請(qǐng)前往知識(shí)社區(qū) showImg(https://segmentfault.com/img/bV8ctF?w=1650&h=1100); 最新資訊 Laravel 5.6 中文文檔翻譯完成,譯者 60 人,耗時(shí) 10 天...
摘要:但是在中,的生命還會(huì)繼續(xù)。這其中最典型的問題便是批量增加元素。這時(shí),如果構(gòu)造函數(shù)被調(diào)用時(shí)沒有參數(shù),則會(huì)自動(dòng)設(shè)置為。因?yàn)閺南到y(tǒng)的角度來說,當(dāng)你用字符串的時(shí)候,它會(huì)被傳進(jìn)構(gòu)造函數(shù),并且重新調(diào)用另一個(gè)函數(shù)。 序言 在今天,JavaScript已經(jīng)成為了網(wǎng)頁編輯的核心。尤其是過去的幾年,互聯(lián)網(wǎng)見證了在SPA開發(fā)、圖形處理、交互等方面大量JS庫的出現(xiàn)。 如果初次打交道,很多人會(huì)覺得js很簡單...
摘要:對(duì)于程序員來說,更意味著代碼的組織,工作成員之間的協(xié)作方式。我常犯的一個(gè)錯(cuò)誤是直接在或分支上直接,而團(tuán)隊(duì)是不允許這樣做的。 先介紹下背景,博主由運(yùn)營轉(zhuǎn)行前端,入職一個(gè)月,完成了一個(gè)相對(duì)較大的模塊。由于基礎(chǔ)相對(duì)薄弱,犯下了不少錯(cuò)誤,故想記錄下來警醒自己和分享各位。 前端技術(shù)棧是 ES6 + backbone + react + antdUI,后端使用的 Ruby on Rails。 1....
摘要:對(duì)于程序員來說,更意味著代碼的組織,工作成員之間的協(xié)作方式。我常犯的一個(gè)錯(cuò)誤是直接在或分支上直接,而團(tuán)隊(duì)是不允許這樣做的。 先介紹下背景,博主由運(yùn)營轉(zhuǎn)行前端,入職一個(gè)月,完成了一個(gè)相對(duì)較大的模塊。由于基礎(chǔ)相對(duì)薄弱,犯下了不少錯(cuò)誤,故想記錄下來警醒自己和分享各位。 前端技術(shù)棧是 ES6 + backbone + react + antdUI,后端使用的 Ruby on Rails。 1....
閱讀 2681·2021-09-13 10:26
閱讀 1932·2021-09-03 10:28
閱讀 2007·2019-08-30 15:44
閱讀 824·2019-08-29 14:07
閱讀 412·2019-08-29 13:12
閱讀 2169·2019-08-26 11:44
閱讀 2361·2019-08-26 11:36
閱讀 2031·2019-08-26 10:19