PHP 語言讓 WEB 端程序設計變得簡單,這也是它能流行起來的原因。但也是因為它的簡單,PHP 也慢慢發(fā)展成一個相對復雜的語言,層出不窮的框架,各種語言特性和版本差異都時常讓搞的我們頭大,不得不浪費大量時間去調(diào)試。這篇文章列出了十個最容易出錯的地方,值得我們?nèi)プ⒁狻?/p>
foreach
循環(huán)后留下數(shù)組的引用還不清楚 PHP 中 foreach
遍歷的工作原理?如果你在想遍歷數(shù)組時操作數(shù)組中每個元素,在 foreach
循環(huán)中使用引用會十分方便,例如
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
$value = $value * 2;
}
// $arr 現(xiàn)在是 array(2, 4, 6, 8)
問題是,如果你不注意的話這會導致一些意想不到的負面作用。在上述例子,在代碼執(zhí)行完以后,$value
仍保留在作用域內(nèi),并保留著對數(shù)組最后一個元素的引用。之后與 $value
相關的操作會無意中修改數(shù)組中最后一個元素的值。
你要記住 foreach
并不會產(chǎn)生一個塊級作用域。因此,在上面例子中 $value
是一個全局引用變量。在 foreach
遍歷中,每一次迭代都會形成一個對 $arr
下一個元素的引用。當遍歷結束后, $value
會引用 $arr
的最后一個元素,并保留在作用域中
這種行為會導致一些不易發(fā)現(xiàn)的,令人困惑的bug,以下是一個例子
$array = [1, 2, 3];
echo implode(',', $array), "\n";
foreach ($array as &$value) {} // 通過引用遍歷
echo implode(',', $array), "\n";
foreach ($array as $value) {} // 通過賦值遍歷
echo implode(',', $array), "\n";
以上代碼會輸出
1,2,3
1,2,3
1,2,2
你沒有看錯,最后一行的最后一個值是 2 ,而不是 3 ,為什么?
在完成第一個 foreach
遍歷后, $array
并沒有改變,但是像上述解釋的那樣, $value
留下了一個對 $array
最后一個元素的危險的引用(因為 foreach
通過引用獲得 $value
)
這導致當運行到第二個 foreach
,這個"奇怪的東西"發(fā)生了。當 $value
通過賦值獲得, foreach
按順序復制每個 $array
的元素到 $value
時,第二個 foreach
里面的細節(jié)是這樣的
第一步:復制 $array[0]
(也就是 1 )到 $value
($value
其實是 $array
最后一個元素的引用,即 $array[2]
),所以 $array[2]
現(xiàn)在等于 1。所以 $array
現(xiàn)在包含 [1, 2, 1]
第二步:復制 $array[1]
(也就是 2 )到 $value
( $array[2]
的引用),所以 $array[2]
現(xiàn)在等于 2。所以 $array
現(xiàn)在包含 [1, 2, 2]
第三步:復制 $array[2]
(現(xiàn)在等于 2 ) 到 $value
( $array[2]
的引用),所以 $array[2]
現(xiàn)在等于 2 。所以 $array
現(xiàn)在包含 [1, 2, 2]
為了在 foreach
中方便的使用引用而免遭這種麻煩,請在 foreach
執(zhí)行完畢后 unset()
掉這個保留著引用的變量。例如
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
$value = $value * 2;
}
unset($value); // $value 不再引用 $arr[3]
isset()
的行為盡管名字叫 isset,但是 isset()
不僅會在變量不存在的時候返回 false
,在變量值為 null
的時候也會返回 false
。
這種行為比最初出現(xiàn)的問題更為棘手,同時也是一種常見的錯誤源。
看看下面的代碼:
$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
// do something here if 'keyShouldBeSet' is not set
}
開發(fā)者想必是想確認 keyShouldBeSet
是否存在于 $data
中。然而,正如上面說的,如果 $data['keyShouldBeSet']
存在并且值為 null
的時候, isset($data['keyShouldBeSet'])
也會返回 false
。所以上面的邏輯是不嚴謹?shù)摹?/p>
我們來看另外一個例子:
if ($_POST@['active']) {
$postData = extractSomething($_POST);
}
// ...
if (!isset($postData)) {
echo 'post not active';
}
上述代碼,通常認為,假如 $_POST@['active']
返回 true
,那么 postData
必將存在,因此 isset($postData)
也將返回 true
。反之, isset($postData)
返回 false
的唯一可能是 $_POST@['active']
也返回 false
。
然而事實并非如此!
如我所言,如果$postData
存在且被設置為 null
, isset($postData)
也會返回 false
。 也就是說,即使 $_POST@['active']
返回 true
, isset($postData)
也可能會返回 false
。 再一次說明上面的邏輯不嚴謹。
順便一提,如果上面代碼的意圖真的是再次確認 $_POST@['active']
是否返回 true
,依賴 isset()
來做,不管對于哪種場景來說都是一種糟糕的決定。更好的做法是再次檢查 $_POST@['active']
,即:
if ($_POST@['active']) {
$postData = extractSomething($_POST);
}
// ...
if ($_POST@['active']) {
echo 'post not active';
}
對于這種情況,雖然檢查一個變量是否真的存在很重要(即:區(qū)分一個變量是未被設置還是被設置為 null
);但是使用 array_key_exists()
這個函數(shù)卻是個更健壯的解決途徑。
比如,我們可以像下面這樣重寫上面第一個例子:
$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
// do this if 'keyShouldBeSet' isn't set
}
另外,通過結合 array_key_exists()
和 get_defined_vars()
, 我們能更加可靠的判斷一個變量在當前作用域中是否存在:
if (array_key_exists('varShouldBeSet', get_defined_vars())) {
// variable $varShouldBeSet exists in current scope
}
考慮下面的代碼片段:
class Config
{
private $values = [];
public function getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
如果你運行上面的代碼,將得到下面的輸出:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
出了什么問題?
上面代碼的問題在于沒有搞清楚通過引用與通過值返回數(shù)組的區(qū)別。除非你明確告訴 PHP 通過引用返回一個數(shù)組(例如,使用 &
),否則 PHP 默認將會「通過值」返回這個數(shù)組。這意味著這個數(shù)組的一份拷貝將會被返回,因此被調(diào)函數(shù)與調(diào)用者所訪問的數(shù)組并不是同樣的數(shù)組實例。
所以上面對 getValues()
的調(diào)用將會返回 $values
數(shù)組的一份拷貝,而不是對它的引用??紤]到這一點,讓我們重新回顧一下以上例子中的兩個關鍵行:
// getValues() 返回了一個 $values 數(shù)組的拷貝
// 所以`test`元素被添加到了這個拷貝中,而不是 $values 數(shù)組本身。
$config->getValues()['test'] = 'test';
// getValues() 又返回了另一份 $values 數(shù)組的拷貝
// 且這份拷貝中并不包含一個`test`元素(這就是為什么我們會得到 「未定義索引」 消息)。
echo $config->getValues()['test'];
一個可能的修改方法是存儲第一次通過 getValues()
返回的 $values
數(shù)組拷貝,然后后續(xù)操作都在那份拷貝上進行;例如:
$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];
這段代碼將會正常工作(例如,它將會輸出test
而不會產(chǎn)生任何「未定義索引」消息),但是這個方法可能并不能滿足你的需求。特別是上面的代碼并不會修改原始的$values
數(shù)組。如果你想要修改原始的數(shù)組(例如添加一個test
元素),就需要修改getValues()
函數(shù),讓它返回一個$values
數(shù)組自身的引用。通過在函數(shù)名前面添加一個&
來說明這個函數(shù)將返回一個引用;例如:
class Config
{
private $values = [];
// 返回一個 $values 數(shù)組的引用
public function &getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
這會輸出期待的test
。
但是現(xiàn)在讓事情更困惑一些,請考慮下面的代碼片段:
class Config
{
private $values;
// 使用數(shù)組對象而不是數(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'];
如果你認為這段代碼會導致與之前的數(shù)組
例子一樣的「未定義索引」錯誤,那就錯了。實際上,這段代碼將會正常運行。原因是,與數(shù)組不同,PHP 永遠會將對象按引用傳遞。(ArrayObject
是一個 SPL 對象,它完全模仿數(shù)組的用法,但是卻是以對象來工作。)
像以上例子說明的,你應該以引用還是拷貝來處理通常不是很明顯就能看出來。因此,理解這些默認的行為(例如,變量和數(shù)組以值傳遞;對象以引用傳遞)并且仔細查看你將要調(diào)用的函數(shù) API 文檔,看看它是返回一個值,數(shù)組的拷貝,數(shù)組的引用或是對象的引用是必要的。
盡管如此,我們要認識到應該盡量避免返回一個數(shù)組或 ArrayObject
,因為這會讓調(diào)用者能夠修改實例對象的私有數(shù)據(jù)。這就破壞了對象的封裝性。所以最好的方式是使用傳統(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』
這個方法讓調(diào)用者可以在不對私有的$values
數(shù)組本身進行公開訪問的情況下設置或者獲取數(shù)組中的任意值。
如果像這樣的話,一定不難見到你的 PHP 無法正常工作。
$models = [];
foreach ($inputValues as $inputValue) {
$models[] = $valueRepository->findByValue($inputValue);
}
這里也許沒有真正的錯誤, 但是如果你跟隨著代碼的邏輯走下去, 你也許會發(fā)現(xiàn)這個看似無害的調(diào)用$valueRepository->findByValue()
最終執(zhí)行了這樣一種查詢,例如:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
結果每輪循環(huán)都會產(chǎn)生一次對數(shù)據(jù)庫的查詢。 因此,假如你為這個循環(huán)提供了一個包含 1000 個值的數(shù)組,它會對資源產(chǎn)生 1000 單獨的請求!如果這樣的腳本在多個線程中被調(diào)用,他會有導致系統(tǒng)崩潰的潛在危險。
因此,至關重要的是,當你的代碼要進行查詢時,應該盡可能的收集需要用到的值,然后在一個查詢中獲取所有結果。
一個我們平時常常能見到查詢效率低下的地方 (例如:在循環(huán)中)是使用一個數(shù)組中的值 (比如說很多的 ID )向表發(fā)起請求。檢索每一個 ID 的所有的數(shù)據(jù),代碼將會迭代這個數(shù)組,每個 ID 進行一次SQL查詢請求,它看起來常常是這樣:
$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;
}
}
因此在你的代碼直接或間接進行查詢請求時,一定要認出這種查詢。盡可能的通過一次查詢得到想要的結果。然而,依然要小心謹慎,不然就可能會出現(xiàn)下面我們要講的另一個易犯的錯誤...
一次取多條記錄肯定是比一條條的取高效,但是當我們使用 PHP 的 mysql
擴展的時候,這也可能成為一個導致 libmysqlclient
出現(xiàn)『內(nèi)存不足』(out of memory)的條件。
我們在一個測試盒里演示一下,該測試盒的環(huán)境是:有限的內(nèi)存(512MB RAM),MySQL,和 php-cli
。
我們將像下面這樣引導一個數(shù)據(jù)表:
// 連接 mysql
$connection = new mysqli('localhost', 'username', 'password', 'database');
// 創(chuàng)建 400 個字段
$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() . "\n";
$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1');
echo "Limit 1: " . memory_get_peak_usage() . "\n";
$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000');
echo "Limit 10000: " . memory_get_peak_usage() . "\n";
輸出結果是:
Before: 224704
Limit 1: 224704
Limit 10000: 224704
Cool。 看來就內(nèi)存使用而言,內(nèi)部安全地管理了這個查詢的內(nèi)存。
為了更加明確這一點,我們把限制提高一倍,使其達到 100,000。 額~如果真這么干了,我們將會得到如下結果:
PHP Warning: mysqli::query(): (HY000/2013):
Lost connection to MySQL server during query in /root/test.php on line 11
究竟發(fā)生了啥?
這就涉及到 PHP 的 mysql
模塊的工作方式的問題了。它其實只是個 libmysqlclient
的代理,專門負責干臟活累活。每查出一部分數(shù)據(jù)后,它就立即把數(shù)據(jù)放入內(nèi)存中。由于這塊內(nèi)存還沒被 PHP 管理,所以,當我們在查詢里增加限制的數(shù)量的時候, memory_get_peak_usage()
不會顯示任何增加的資源使用情況 。我們被『內(nèi)存管理沒問題』這種自滿的思想所欺騙了,所以才會導致上面的演示出現(xiàn)那種問題。 老實說,我們的內(nèi)存管理確實是有缺陷的,并且我們也會遇到如上所示的問題。
如果使用 mysqlnd
模塊的話,你至少可以避免上面那種欺騙(盡管它自身并不會提升你的內(nèi)存利用率)。 mysqlnd
被編譯成原生的 PHP 擴展,并且確實 會 使用 PHP 的內(nèi)存管理器。
因此,如果使用 mysqlnd
而不是 mysql
,我們將會得到更真實的內(nèi)存利用率的信息:
Before: 232048
Limit 1: 324952
Limit 10000: 32572912
順便一提,這比剛才更糟糕。根據(jù) PHP 的文檔所說,mysql
使用 mysqlnd
兩倍的內(nèi)存來存儲數(shù)據(jù), 所以,原來使用 mysql
那個腳本真正使用的內(nèi)存比這里顯示的更多(大約是兩倍)。
為了避免出現(xiàn)這種問題,考慮限制一下你查詢的數(shù)量,使用一個較小的數(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");
}
當我們把這個常見錯誤和上面的 常見錯誤 #4 結合起來考慮的時候, 就會意識到我們的代碼理想需要在兩者間實現(xiàn)一個平衡。是讓查詢粒度化和重復化,還是讓單個查詢巨大化。生活亦是如此,平衡不可或缺;哪一個極端都不好,都可能會導致 PHP 無法正常運行。
從某種意義上說,這實際上是PHP本身的一個問題,而不是你在調(diào)試 PHP 時遇到的問題,但是它從未得到妥善的解決。 PHP 6 的核心就是要做到支持 Unicode。但是隨著 PHP 6 在 2010 年的暫停而擱置了。
這并不意味著開發(fā)者能夠避免 正確處理 UTF-8 并避免做出所有字符串必須是『古老的 ASCII』的假設。 沒有正確處理非 ASCII 字符串的代碼會因為引入粗糙的 海森堡bug(heisenbugs) 而變得臭名昭著。當一個名字包含 『Schr?dinger』的人注冊到你的系統(tǒng)時,即使簡單的 strlen($_POST@['name'])
調(diào)用也會出現(xiàn)問題。
下面是一些可以避免出現(xiàn)這種問題的清單:
如果你對 UTF-8 還不了解,那么你至少應該了解下基礎的東西。 這兒 有個很好的引子。
確保使用 mb_*
函數(shù)代替老舊的字符串處理函數(shù)(需要先保證你的 PHP 構建版本開啟了『多字節(jié)』(multibyte)擴展)。
確保你的數(shù)據(jù)庫和表設置了 Unicode 編碼(許多 MySQL 的構建版本仍然默認使用 latin1
)。
記住 json_encode()
會轉(zhuǎn)換非 ASCII 標識(比如: 『Schr?dinger』會被轉(zhuǎn)換成 『Schru00f6dinger』),但是 serialize()
不會 轉(zhuǎn)換。
確保 PHP 文件也是 UTF-8 編碼,以避免在連接硬編碼字符串或者配置字符串常量的時候產(chǎn)生沖突。
Francisco Claria 在本博客上發(fā)表的 UTF-8 Primer for PHP and MySQL 是份寶貴的資源。
$_POST
總是包含你 POST 的數(shù)據(jù)不管它的名稱,$_POST
數(shù)組不是總是包含你 POST 的數(shù)據(jù),他也有可能會是空的。 為了理解這一點,讓我們來看一下下面這個例子。假設我們使用 jQuery.ajax()
模擬一個服務請求,如下:
// 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 里是默認的發(fā)送數(shù)據(jù)的類型。)
在我們舉例子的服務端,我們簡單的打印一下 $_POST
數(shù)組:
// php
var_dump($_POST);
奇怪的是,結果如下:
array(0) { }
為什么?我們的 JSON 串 {a: 'a', b: 'b'}
究竟發(fā)生了什么?
原因在于 當內(nèi)容類型為 application/x-www-form-urlencoded
或者 multipart/form-data
的時候 PHP 只會自動解析一個 POST 的有效內(nèi)容。這里面有歷史的原因 --- 這兩種內(nèi)容類型是在 PHP 的 $_POST
實現(xiàn)前就已經(jīng)在使用了的兩個重要的類型。所以不管使用其他任何內(nèi)容類型 (即使是那些現(xiàn)在很流行的,像 application/json
), PHP 也不會自動加載到 POST 的有效內(nèi)容。
既然 $_POST
是一個超級全局變量,如果我們重寫 一次 (在我們的腳本里盡可能早的),被修改的值(包括 POST 的有效內(nèi)容)將可以在我們的代碼里被引用。這很重要因為 $_POST
已經(jīng)被 PHP 框架和幾乎所有的自定義的腳本普遍使用來獲取和傳遞請求數(shù)據(jù)。
所以,舉個例子,當處理一個內(nèi)容類型為 application/json
的 POST 有效內(nèi)容的時候 ,我們需要手動解析請求內(nèi)容(decode 出 JSON 數(shù)據(jù))并且覆蓋 $_POST
變量,如下:
// php
$_POST = json_decode(file_get_contents('php://input'), true);
然后當我們打印 $_POST
數(shù)組的時候,我們可以看到他正確的包含了 POST 的有效內(nèi)容;如下:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
閱讀下面的代碼并思考會輸出什么:
for ($c = 'a'; $c <= 'z'; $c++) {
echo $c . "\n";
}
如果你的答案是 a
到 z
,那么你可能會對這是一個錯誤答案感到吃驚。
沒錯,它確實會輸出 a
到 z
,但是,它還會繼續(xù)輸出 aa
到 yz
。我們一起來看一下這是為什么。
PHP 中沒有 char
數(shù)據(jù)類型; 只能用 string
類型。記住一點,在 PHP 中增加 string
類型的 z
得到的是 aa
:
php> $c = 'z'; echo ++$c . "\n";
aa
沒那么令人混淆的是,aa
的字典順序是 小于 z
的:
php> var_export((boolean)('aa' < 'z')) . "\n";
true
這也是為什么上面那段簡單的代碼會輸出 a
到 z
, 然后 繼續(xù) 輸出 aa
到 yz
。 它停在了 za
,那是它遇到的第一個比 z
大 的:
php> var_export((boolean)('za' < 'z')) . "\n";
false
事實上,在 PHP 里 有合適的 方式在循環(huán)中輸出 a
到 z
的值:
for ($i = ord('a'); $i <= ord('z'); $i++) {
echo chr($i) . "\n";
}
或者是這樣:
$letters = range('a', 'z');
for ($i = 0; $i < count($letters); $i++) {
echo $letters[$i] . "\n";
}
盡管忽視代碼標準并不直接導致需要去調(diào)試 PHP 代碼,但這可能是所有需要談論的事情里最重要的一項。
在一個項目中忽視代碼規(guī)范能夠?qū)е麓罅康膯栴}。最樂觀的預計,前后代碼不一致(在此之前每個開發(fā)者都在“做自己的事情”)。但最差的結果,PHP 代碼不能運行或者很難(有時是不可能的)去順利通過,這對于 調(diào)試代碼、提升性能、維護項目來說也是困難重重。并且這意味著降低你們團隊的生產(chǎn)力,增加大量的額外(或者至少是本不必要的)精力消耗。
幸運的是對于 PHP 開發(fā)者來說,存在 PHP 編碼標準建議(PSR),它由下面的五個標準組成:
PSR 起初是由市場上最大的組織平臺維護者創(chuàng)造的。 Zend, Drupal, Symfony, Joomla 和 其他 為這些標準做出了貢獻,并一直遵守它們。甚至,多年前試圖成為一個標準的 PEAR ,現(xiàn)在也加入到 PSR 中來。
某種意義上,你的代碼標準是什么幾乎是不重要的,只要你遵循一個標準并堅持下去,但一般來講,跟隨 PSR 是一個很不錯的主意,除非你的項目上有其他讓人難以抗拒的理由。越來越多的團隊和項目正在遵從 PSR 。在這一點上,大部分的 PHP 開發(fā)者達成了共識,因此使用 PSR 代碼標準,有利于使新加入團隊的開發(fā)者對你的代碼標準感到更加的熟悉與舒適。
empty()
一些 PHP 開發(fā)者喜歡對幾乎所有的事情使用 empty()
做布爾值檢驗。不過,在一些情況下,這會導致混亂。
首先,讓我們回到數(shù)組和 ArrayObject
實例(和數(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之前的結果可能是不同的:
// PHP 5.0 之前:
$array = [];
var_dump(empty($array)); // 輸出 bool(false)
$array = new ArrayObject();
var_dump(empty($array)); // 輸出 bool(false)
這種方法上的不幸是十分普遍的。比如,在 Zend Framework 2 下的 Zend\Db\TableGateway
的 TableGateway::select()
結果中調(diào)用 current()
時返回數(shù)據(jù)的方式,正如文檔所表明的那樣。開發(fā)者很容易就會變成此類數(shù)據(jù)錯誤的受害者。
為了避免這些問題的產(chǎn)生,更好的方法是使用 count()
去檢驗空數(shù)組結構:
// 注意這會在 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)部去檢驗空數(shù)組。同樣值得注意的是,在 PHP 中, count()
在數(shù)組中是常量復雜度 (O(1)
操作) ,這更清晰的表明它是正確的選擇。
另一個使用 empty()
產(chǎn)生危險的例子是當它和魔術方法 _get()
一起使用。我們來定義兩個類并使其都有一個 test
屬性。
首先我們定義包含 test
公共屬性的 Regular
類。
class Regular
{
public $test = 'value';
}
然后我們定義 Magic
類,這里使用魔術方法 __get()
來操作去訪問它的 test
屬性:
class Magic
{
private $values = ['test' => 'value'];
public function __get($key)
{
if (isset($this->values[$key])) {
return $this->values[$key];
}
}
}
好了,現(xiàn)在我們嘗試去訪問每個類中的 test
屬性看看會發(fā)生什么:
$regular = new Regular();
var_dump($regular->test); // 輸出 string(4) "value"
$magic = new Magic();
var_dump($magic->test); // 輸出 string(4) "value"
到目前為止還好。
但是現(xiàn)在當我們對其中的每一個都調(diào)用 empty()
,讓我們看看會發(fā)生什么:
var_dump(empty($regular->test)); // 輸出 bool(false)
var_dump(empty($magic->test)); // 輸出 bool(true)
咳。所以如果我們依賴 empty()
,我們很可能誤認為 $magic
的屬性 test
是空的,而實際上它被設置為 'value'
。
不幸的是,如果類使用魔術方法 __get()
來獲取屬性值,那么就沒有萬無一失的方法來檢查該屬性值是否為空。
在類的作用域之外,你僅僅只能檢查是否將返回一個 null
值,這并不意味著沒有設置相應的鍵,因為它實際上還可能被設置為 null
。
相反,如果我們試圖去引用 Regular
類實例中不存在的屬性,我們將得到一個類似于以下內(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
所以這里的主要觀點是 empty()
方法應該被謹慎地使用,因為如果不小心的話它可能導致混亂 -- 甚至潛在的誤導 -- 結果。
PHP 的易用性讓開發(fā)者陷入一種虛假的舒適感,語言本身的一些細微差別和特質(zhì),可能花費掉你大量的時間去調(diào)試。這些可能會導致 PHP 程序無法正常工作,并導致諸如此處所述的問題。
PHP 在其20年的歷史中,已經(jīng)發(fā)生了顯著的變化?;〞r間去熟悉語言本身的微妙之處是值得的,因為它有助于確保你編寫的軟件更具可擴展性,健壯和可維護性。