前言
PHP 的弱類型特性導致比較運算極其複雜,是 Bug 高發區。本文系統梳理類型轉換規則、比較語義、邏輯運算優先級、短路求值特性,並提供決策表與最佳實踐,幫助開發者寫出可預測、無驚喜的代碼。
一、empty() 與 isset() 判斷真值表
| 值 | empty() | isset() | boolval() |
| 未定義變量 | true (Notice) | false | false |
| null | true | false | false |
| false | true | true | false |
| 0 (int) | true | true | false |
| 0.0 (float) | true | true | false |
| '0' (string) | true | true | false |
| '' (空字符串) | true | true | false |
| [] (空數組) | true | true | false |
| SimpleXML 空元素 | true | false | false |
| 無屬性對象 (PHP<5.0) | true | true | true |
| __get() 返回上述值 | true | false | 依具體值 |
關鍵差異
empty():檢查「是否為空值」,不觸發 Notice,可用於未定義變量
isset():檢查「是否已定義且非 null」,不觸發 Notice
array_key_exists():檢查「鍵是否存在」,包含 null 值
<?php
$a = ['key' => null];
empty($a['key']); // true
isset($a['key']); // false
array_key_exists('key', $a); // true
// __get 魔術方法陷阱
class Test {
public function __get($name) { return null; }
}
$obj = new Test();
empty($obj->prop); // true (調用 __get 返回 null)
isset($obj->prop); // false (調用 __get 返回 null)
?>
二、類型轉換規則總表
標量轉換矩陣
| 原值 | (bool) | (int) | (float) | (string) | (array) | (object) |
| null | false | 0 | 0.0 | '' | [] | stdClass{} |
| false | false | 0 | 0.0 | '' | [0=>false] | stdClass{scalar:false} |
| true | true | 1 | 1.0 | '1' | [0=>true] | stdClass{scalar:true} |
| 0 | false | 0 | 0.0 | '0' | [0=>0] | stdClass{scalar:0} |
| 1 | true | 1 | 1.0 | '1' | [0=>1] | stdClass{scalar:1} |
| 'abc' | true | 0 | 0.0 | 'abc' | [0=>'abc'] | stdClass{scalar:'abc'} |
| '123' | true | 123 | 123.0 | '123' | [0=>'123'] | stdClass{scalar:'123'} |
| '0x12' | true | 0 | 0.0 | '0x12' | [0=>'0x12'] | stdClass{scalar:'0x12'} |
| [] | false | 0 | 0.0 | 'Array' (Notice) | [] | stdClass{} |
| [1,2] | true | 1 | 1.0 | 'Array' (Notice) | [1,2] | stdClass{0=>1,1=>2} |
字符串轉數字細節
<?php
// 規則:從左向右提取數字字符,遇非數字停止
// 十六進制僅在數學運算/特定函數中識別
var_dump('123abc' + 0); // int(123)
var_dump('0x12' + 0); // int(18) 數學運算識別十六進制
var_dump((int)'0x12'); // int(0) 強制轉換不識別
var_dump((float)'0x12'); // float(0)
var_dump(intval('0x12')); // int(0)
var_dump(is_numeric('0x12')); // true is_numeric 識別十六進制
var_dump(is_numeric('0x12.3')); // false 不識別十六進制小數
// 科學計數法
var_dump('1.2e3' + 0); // float(1200)
var_dump('1.2E-3' + 0); // float(0.0012)
?>
三、比較運算符語義
== (鬆散比較) vs === (嚴格比較)
| 表達式 | 結果 | 說明 |
| 0 == '0' | true | 字符串轉數字 |
| 0 === '0' | false | 類型不同 |
| '0' == false | true | 都轉 bool |
| null == false | true | 都轉 bool |
| null === false | false | 類型不同 |
| [] == false | true | 空數組轉 bool |
| 'abc' == 0 | true | 字符串轉數字得 0 |
| 'abc' == '0' | false | 字符串比較 |
| 123 == '0123' | true | 前導零不視為八進制 |
| '0123' == '123' | false | 字符串比較 |
| 0.1 + 0.2 == 0.3 | false | 浮點精度問題 |
比較決策流程(內部邏輯)
graph TD
A[比較 $a == $b] --> B{類型相同?}
B -->|是| C[直接比較值]
B -->|否| D{有 bool/null?}
D -->|是| E[雙方轉 bool 比較]
D -->|否| F{有數字類型?}
F -->|是| G[雙方轉數字比較]
F -->|否| H[雙方轉字符串比較]
C --> I[數字: 數值比較]
C --> J[字符串: 字典序]
C --> K[數組: 鍵值對比較]
C --> L[對象: 屬性比較(同類) / 類名比較(異類)]
數組比較規則
<?php
$a = [1, 2, 3];
$b = [1, 2, 3];
$c = ['a' => 1, 'b' => 2];
$d = ['b' => 2, 'a' => 1];
var_dump($a == $b); // true (鍵值順序相同)
var_dump($a === $b); // true
var_dump($c == $d); // true (鍵值相同,順序無關)
var_dump($c === $d); // false (鍵順序不同)
$a = [1 => 'a', 0 => 'b'];
$b = [0 => 'b', 1 => 'a'];
var_dump($a == $b); // true
var_dump($a === $b); // false (鍵順序不同)
?>
四、邏輯運算符優先級與短路求值
優先級表(高到低)
<?php
// 1. clone new
// 2. [] (array)
// 3. ** (冪)
// 4. ++ -- ~ (一元)
// 5. instanceof
// 6. * / %
// 7. + - .
// 8. << >>
// 9. < <= > >=
// 10. == != === !== <> <=> (比較)
// 11. & (位與)
// 12. ^ (位異或)
// 13. | (位或)
// 14. && (邏輯與) ← 關鍵:比 and 高
// 15. || (邏輯或) ← 關鍵:比 or 高
// 16. ?? (空合並)
// 17. ? : (三元) ← 右結合!
// 18. = += -= *= /= .= %= &= |= ^= <<= >>= ??=
// 19. and (邏輯與) ← 極低優先級
// 20. xor (邏輯異或)
// 21. or (邏輯或) ← 極低優先級
經典陷阱:&& vs and
<?php
// 情況 1:賦值表達式
$a = 1;
$b = 0;
$b && $a = 100; // 等價 $b && ($a = 100)
echo $a; // 1 (短路,$a 未賦值)
$b || $a = 200; // 等價 $b || ($a = 200)
echo $a; // 200 (執行賦值)
// 情況 2:函數調用鏈
function f1() { echo "f1\n"; return false; }
function f2() { echo "f2\n"; return true; }
f1() && f2(); // 輸出 f1 (短路)
f1() and f2(); // 輸出 f1\nf2 (and 優先級極低,等價 f1() and (f2()))
// 建議:統一使用 && || ,避免 and or xor
?>
三元運算符右結合陷阱
<?php
// PHP 三元運算符是左結合(大多數語言右結合)
$arg = 'T';
$vehicle = ($arg == 'B') ? 'bus' :
($arg == 'A') ? 'airplane' :
($arg == 'T') ? 'train' :
($arg == 'C') ? 'car' : 'feet';
echo $vehicle; // 輸出 'horse' 而非 'train'!
// 解析過程(左結合):
// (((($arg == 'B') ? 'bus' : ($arg == 'A')) ? 'airplane' : ($arg == 'T')) ? 'train' : ...)
// 實際計算:
// 1. ($arg == 'B') ? 'bus' : ($arg == 'A') → false ? 'bus' : false → false
// 2. false ? 'airplane' : ($arg == 'T') → false ? ... : true → true
// 3. true ? 'train' : ($arg == 'C') → 'train' ? ... : false → 'train'
// 4. 'train' ? 'car' : 'feet' → 'train' (非空字符串為 true) → 'train'
// 等等... 實際輸出 'horse' 是舊版本行為,PHP 8.0+ 棄用無括號鏈式三元
// 正確寫法:必須加括號
$vehicle = ($arg == 'B') ? 'bus' :
(($arg == 'A') ? 'airplane' :
(($arg == 'T') ? 'train' :
(($arg == 'C') ? 'car' : 'feet')));
// PHP 8.0+ 建議:使用 match 表達式
$vehicle = match($arg) {
'B' => 'bus',
'A' => 'airplane',
'T' => 'train',
'C' => 'car',
default => 'feet',
};
?>
五、switch 比較語義
<?php
// switch 使用 == (鬆散比較),而非 ===
$value = '0abc';
switch ($value) {
case 0:
echo "匹配 0\n"; // 會執行!'0abc' == 0 為 true
break;
case '0abc':
echo "匹配 '0abc'\n";
break;
}
// 解決方案:強制類型或使用 match (PHP 8.0+)
switch (true) {
case $value === 0:
case $value === '0abc':
// ...
}
// match 使用 === (嚴格比較)
$result = match($value) {
0 => 'int zero',
'0abc' => 'string 0abc',
default => 'other',
};
?>
六、對象比較規則
<?php
class A { public $x = 1; }
class B { public $x = 1; }
$a1 = new A();
$a2 = new A();
$a3 = $a1;
$b = new B();
var_dump($a1 == $a2); // true (同類、屬性相同)
var_dump($a1 === $a2); // false (不同實例)
var_dump($a1 === $a3); // true (同一實例)
var_dump($a1 == $b); // false (不同類)
var_dump($a1 === $b); // false
// PHP 8.0+ 可實現 __equals() 自定義比較
class CustomCompare {
public function __construct(public int $id) {}
public function __equals(object $other): bool {
return $other instanceof self && $other->id === $this->id;
}
}
?>
七、對象轉數組完整規則
<?php
class Demo {
public $pub = 'public';
protected $pro = 'protected';
private $pri = 'private';
static $sta = 'static';
const CONST = 'const';
}
$obj = new Demo();
$arr = (array) $obj;
print_r($arr);
/*
Array
(
[pub] => public
[*pro] => protected // 前綴 * + \0
[Demopri] => private // 類名 + \0
)
*/
// 關鍵點:
// 1. 靜態屬性、常量、方法被丟棄
// 2. protected 前綴: \0*\0
// 3. private 前綴: \0類名\0
// 4. 這些特殊字符不可見,需用 var_dump/strlen 觀察
?>
八、最佳實踐清單
| 場景 | 推薦做法 | 避免 |
| 判斷變量是否有值 | isset($var) || $var !== null | empty($var) (會吞掉 '0') |
| 判斷數組非空 | $arr !== [] 或 count($arr) > 0 | empty($arr) (同樣問題) |
| 比較數值 | $a === $b (嚴格) | == (類型轉換陷阱) |
| 浮點數比較 | abs($a - $b) < 0.0001 | == / === |
| 字符串比較 | strcmp($a, $b) === 0 | == (數字字符串陷阱) |
| 邏輯組合 | $a && $b / $a || $b | and / or (優先級陷阱) |
| 多條件分支 | match (PHP 8.0+) / switch(true) | switch($var) 鬆散比較 |
| 默認值賦值 | $val = $input ?? 'default' | $val = $input ?: 'default' (0 會被覆蓋) |
總結
PHP 比較運算的核心矛盾在於弱類型自動轉換與開發者心理模型的偏差。黃金法則:默認使用 === 和 !==、邏輯用 && ||、分支用 match、浮點用 epsilon 比較、字符串用 strcmp、默認值用 ??。只有在明確需要類型轉換語義時(如 HTTP 參數解析),才有意識地使用鬆散比較,並添加註釋說明意圖。