isset() 與 empty() 的差異

PHP 的 isset()empty() 是解析變數常用的語言結構,兩者用法很接近,有時一不小心就會誤用。

這是一個老掉牙的主題,隨便 Google 都一堆比較文,官網也有描述它們的差異。但我還是決定重覆造輪子,主要是想把 array_key_exists() 函式拿來一起討論。

同場加映:??(Null coalescing operator)與 property_exists() 函式的比較

2021/10/3 補充:舊版 PHP 的 isset() 其他情境。

先來看一下這三個功能在使用上的差異,為什麼用「功能」來描述它們?因為 isset() 和 empty() 是語言結構(Language construct),而 array_key_exists() 是函式(function),所以用「功能」來統稱它們。

以下程式碼區塊為 Psy Shell 實驗複制下來的,環境為:

Psy Shell v0.10.8 (PHP 8.0.9 — cli) by Justin Hileman

isset()

isset() 語意上是在判斷變數是否被設置,所以只能傳變數,而不能直接傳值,直接傳值會拋出 Fatal error:

>>> isset(0)
PHP Fatal error: Cannot use isset() on the result of an expression (you can use "null !== expression" instead) in Psy Shell code on line 1
>>> $v = 0;isset($v)
=> true

會回傳 true 的情境包括 0、空字串、空陣列等,因為這些都是變數已存在的情境;相對地,回傳 false 的情境只有兩個:

  1. 變數未設置,類似 Javascript 的 undefined(註一)
  2. 變數值是 null

註一:根據前輩的經驗指出,舊版 PHP 無法正常處理變數未設置的狀況而出錯,但不知道是哪一版。雖然查過官網也沒有相關資訊,不過依筆者經驗從 PHP 5.3 開始,在這版本以上都是如文章所述。

當嘗試去使用一個未設置的變數時,會丟出一個 Warning 的訊息,並且回傳 null,而 isset() 可以把 Warning 的訊息吃掉:

>>> $a
<warning>PHP Warning: Undefined variable $a in /tmp code on line 1</warning>
=> null
>>> isset($a)
=> false

再來有個特別的狀況:is_null() 官網有拿來一起比較,注意看會發現,isset() 的真值表剛好跟 is_null() 完全相反,它們有三個地方不同:

  1. isset() 是語法結構;is_null() 是函式,所以 isset() 會比較快一點 但推測 compiler 已有做最佳化,測試的結果是差不多的。
  2. isset() 在變數沒定義時,不會出任何錯誤;is_null() 會出現 PHP Warning
  3. isset() 必須要傳變數,不能傳實字常數(literal constants),這在開頭有說明;is_null() 可以傳變數與實字常數,如 is_null(null)

而以 isset() 與 is_null() 來說,大多數情境都是用 isset() 居多,主要是當變數沒定義時,可以確保不會出錯;另一方面是因為 is_null($value) 其實跟 $value === null 意義一樣,後者的效能跟 isset() 差不多,因為呼叫函式還會多一層 overhead。

當變數可能沒定義的情境,建議使用 isset(),可以確保不會噴 Warning 訊息;而想確認變數是否為 null 的話,則建議使用 $value === null,因為 is_null() 在 namespace 下使用會變慢,需要加 use function is_null; 才會正常,但一般都會忘了加,如同本篇文章一開始做測試一樣。

+------------------+-----------------------+-----------+---------+---------+---------+---------+---------+
| benchmark | subject | memory | min | max | mode | rstdev | stdev |
+------------------+-----------------------+-----------+---------+---------+---------+---------+---------+
| IssetIsNullBench | benchIsset () | 966.360kb | 0.035μs | 0.045μs | 0.036μs | ±10.54% | 0.004μs |
| IssetIsNullBench | benchIsNull () | 966.360kb | 0.033μs | 0.035μs | 0.033μs | ±2.11% | 0.001μs |
| IssetIsNullBench | benchConditionNull () | 966.376kb | 0.032μs | 0.035μs | 0.034μs | ±2.61% | 0.001μs |
+------------------+-----------------------+-----------+---------+---------+---------+---------+---------+

測試程式: https://github.com/MilesChou/php-notice/blob/master/benchmarks/IssetIsNullBench.php

9/1 更新:從 opcodes 可以確定 is_null($v) 效能跟 $v === null 效能相同,因此上文有針對此部分調整說明。

empty()

empty() 語意上是在看變數是否是空的,在 PHP 5.5 之前是不能傳值的,5.5 開始可以接受傳值。

會回傳 true 的情境如變數未設定,這跟 isset() 剛好相反的;而如果是在判斷未定義這件事的話,isset() 和 empty() 的效能是差不多的。

效能可參考最下面的附表一:效能比較

但問題在於,empty() 當 0、空字串、空陣列等情境(可參考官網),也會回傳 true,因為它們確實是「空」的,這些判斷方法就跟 isset() 不一樣了。

empty() 與 isset() 雖然這兩個語言結構都支援未定義變數,但對於判斷資料型態是否為「空」,用法完全不同,因此如果要針對資料型態資料做空的判斷時,才需要使用 empty();判斷變數是否有設值,還是要用 isset()。

變數定義相較是靜態的,開發者如果有配合 IDE 或靜態分析去改善寫法的話,undefined 的情境通常都能抓得到,不大需要用到 isset(),需要這個判斷通常會是 array。

array_key_exists()

isset() 可以確認 array 的一個 key 是否有設值,而有另一個函式:array_key_exists() 與 isset() 非常像。差異直接使用舉例說明,當 array 某個元素是 null 的時候:

  1. isset() 會回傳 false
  2. array_key_exists() 會回傳 true

可參考最下面的附表二:真值表

因此這兩個功能的用法又有點不大一樣,在實作 key 是否存在的判斷時,記得它們在值是 null 的情境下,會有不同的回傳值。

總結

isset() 和 empty() 在 IDE 與靜態分析的協助下,一般不大需要對變數做判斷,因此主要會是應用在 array 的場景。如果是 array 場景,會有另一個函式 array_key_exists() 可以參考使用,這三個功能在 array 的應用場景如下:

  1. empty() 用在對資料型態是否為空的判斷
  2. isset() 可判斷 array key 是否已定義,而值是 null 的時候會是未定義(false)
  3. array_key_exists() 可判斷 array key 是否存在,當值是 null 的時候會是存在(true)

最後分享一下,最近常見到兩個類似的寫法如下:

if (isset($arr['foo']) || !empty($arr['foo'])) {
// do something
}

看完本篇說明後可以知道,跟下面這個寫法是一模一樣的:

if (isset($arr['foo'])) {
// do something
}

NOT 運算對調,其實也是類似的,只是結果變成跟 empty() 相同:

if (!isset($arr['foo']) || empty($arr['foo'])) {
// do something
}

跟下面這個寫法一模一樣:

if (empty($arr['foo'])) {
// do something
}

測試程式碼: https://github.com/MilesChou/php-notice/blob/master/tests/IssetEmptyTest.php

同時用了兩個判斷的人,建議還是要好好思考到底需求為何,到底是要 isset() 還是 empty(),不然會讓閱讀程式的人(或未來的自己)搞不懂要做什麼。

附表一:效能比較

效能測試了一下,array_key_exists() 因為是函式,所以比較慢;isset() 和 empty() 則差不多:

+---------------------+------------------------------------+-----------+---------+---------+---------+--------+---------+
| benchmark | subject | memory | min | max | mode | rstdev | stdev |
+---------------------+------------------------------------+-----------+---------+---------+---------+--------+---------+
| ArrayKeyExistsBench | benchIssetWhenNotExist () | 970.680kb | 0.463μs | 0.494μs | 0.480μs | ±2.20% | 0.011μs |
| ArrayKeyExistsBench | benchIssetWhenExist () | 970.680kb | 0.460μs | 0.507μs | 0.504μs | ±4.43% | 0.022μs |
| ArrayKeyExistsBench | benchEmptyWhenNotExist () | 970.680kb | 0.446μs | 0.541μs | 0.518μs | ±6.39% | 0.032μs |
| ArrayKeyExistsBench | benchEmptyWhenExist () | 970.680kb | 0.522μs | 0.585μs | 0.537μs | ±4.25% | 0.023μs |
| ArrayKeyExistsBench | benchArrayKeyExistsWhenNotExist () | 970.696kb | 0.950μs | 1.131μs | 0.993μs | ±6.57% | 0.067μs |
| ArrayKeyExistsBench | benchArrayKeyExistsWhenExist () | 970.696kb | 1.018μs | 1.185μs | 1.057μs | ±5.22% | 0.056μs |
+---------------------+------------------------------------+-----------+---------+---------+---------+--------+---------+

測試程式連結: https://github.com/MilesChou/php-notice/blob/master/benchmarks/ArrayKeyExistsBench.php

附表二:真值表

2021/9/30 新增 property_exists() 函式的真值表

$v 的內容 isset($v['foo']) empty($v['foo']) array_key_exists('foo', $v) property_exists((object)$v, 'foo')
[] false true false false
['foo' => null] false true true true
['foo' => ''] true true true true
['foo' => []] true true true true
['foo' => ['a', 'b']] true false true true
['foo' => false] true true true true
['foo' => true] true false true true
['foo' => 1] true false true true
['foo' => 0] true true true true
['foo' => -1] true false true true
['foo' => "1"] true false true true
['foo' => "0"] true true true true
['foo' => "-1"] true false true true
['foo' => "php"] true false true true
['foo' => "true"] true false true true
['foo' => "false"] true false true true

參考連結