式怎么用才規(guī)范)
前言先說一個版本事實match表達(dá)式是 PHP 8.0 引入的RFC 名稱是matchexpression v2不是 PHP 8.1。標(biāo)題里的版號有誤本文按 8.0 講文中代碼在 8.0、8.1 一直到 8.5 上都能直接運行。這個混淆很常見大概是因為 8.1 同時帶來了枚舉Enum而match和枚舉經(jīng)常成對出現(xiàn)容易被記成同一批特性。從switch換到match之后最常見的三種癥狀是程序突然拋UnhandledMatchError而以前switch什么都不做match(1)的某個分支明明寫了1卻永遠(yuǎn)不命中以及用catch (Exception $e)兜底卻完全接不住這個錯誤。這三個癥狀分別對應(yīng)match的三個核心設(shè)計窮盡性檢查、嚴(yán)格比較、錯誤繼承自Error。理解這三條match就會從偶爾炸的語法糖變成能在編譯期和運行期幫你抓 bug 的表達(dá)式。一、match 與 switch 的六點差異維度switchmatch本質(zhì)語句statement表達(dá)式expression有返回值比較方式松散比較嚴(yán)格比較穿透不寫break會穿透不會穿透天然隔離未命中靜默跳過無default時拋UnhandledMatchError分支內(nèi)容任意語句塊必須是單個表達(dá)式多值分支只能靠穿透堆疊case用逗號1, 2, 3 x求值依次執(zhí)行case判斷只求值命中的那個臂返回值無直接返回可參與表達(dá)式只求值命中的那個臂這一條很實用match ($x) { 1 expensive(), 2 cheap() }在$x 2時不會執(zhí)行expensive()而switch里如果邏輯寫在外部函數(shù)調(diào)用位置就未必享受得到這種惰性。還有一條容易忽略的細(xì)節(jié)match的分支列表里逗號分隔的多個條件是或的關(guān)系而且這些條件本身是表達(dá)式可以用常量、甚至可以用比較表達(dá)式——這也是后面match(true)技巧的基礎(chǔ)。二、嚴(yán)格比較帶來的連鎖反應(yīng)match用比較意味著類型必須完全一致。這一條會以三種形式咬人?php declare(strict_types1); // 1. 字符串?dāng)?shù)字 vs 整數(shù) echo match (1) { 1 整數(shù)一, // 不命中1 ! 1 default 未命中, }, PHP_EOL; // 輸出未命中 // 2. 整數(shù) vs 浮點 echo match (1.0) { 1 整數(shù)一, // 不命中1.0 ! 1 default 未命中, }, PHP_EOL; // 輸出未命中 // 3. 與比較表達(dá)式混用時要顯式轉(zhuǎn)型 $raw 42; echo match (true) { (int) $raw 40 大數(shù), default 小數(shù), }, PHP_EOL; // 輸出大數(shù)從 HTTP 請求、環(huán)境變量、配置文件里讀出來的值幾乎都是字符串直接丟給match用整數(shù)分支接就會穩(wěn)定地落到default如果沒有default就是UnhandledMatchError。規(guī)范做法是在進(jìn)match之前先做一次顯式類型轉(zhuǎn)換把類型不確定性收斂到一個點上。三、match(true) 與枚舉搭配match(true)是把match當(dāng)多分支 if用的官方常見技巧每個臂寫一個布爾表達(dá)式第一個為true的臂生效。?php declare(strict_types1); function levelOf(int $score): string { // match(true) 的每個臂都是一個布爾判斷從上到下取第一個為 true 的 return match (true) { $score 90 A, $score 80 B, $score 60 C, $score 0 D, default 非法分?jǐn)?shù), }; } echo levelOf(95), levelOf(85), levelOf(72), levelOf(30), PHP_EOL; // ABCD echo levelOf(-1), PHP_EOL; // 非法分?jǐn)?shù)順序在這里極其重要match(true)是從上往下取第一個命中的臂把范圍寬的判斷寫在前面后面就永遠(yuǎn)不會命中。上面如果把$score 0寫在最前面所有分?jǐn)?shù)都會變成D。與枚舉PHP 8.1搭配時match ($this)是最推薦的寫法因為它由引擎保證窮盡性——新增一個枚舉 case 卻忘了更新match運行時立刻拋錯而不是悄悄走到某個兜底分支?php declare(strict_types1); enum Role: string { case Admin admin; case Editor editor; case Viewer viewer; public function canPublish(): bool { // 不寫 default讓漏了 case變成顯式錯誤 return match ($this) { self::Admin, self::Editor true, self::Viewer false, }; } }這也是match最值得強調(diào)的用法不要用default去掩蓋遺漏。對枚舉這類取值集合封閉的場景省略default是正確選擇對取值來自外部的場景HTTP 狀態(tài)碼、第三方返回碼則必須寫default。四、完整可運行示例?php declare(strict_types1); // 需要 PHP 8.0match 表達(dá)式在 8.1 ~ 8.5 上同樣可用 enum Method: string { case Get GET; case Post POST; case Put PUT; case Delete DELETE; public function isSafe(): bool { return match ($this) { self::Get true, self::Post, self::Put, self::Delete false, }; } /** 外部字符串 - 枚舉 - 行為類型轉(zhuǎn)換只在一個地方做 */ public static function fromRaw(string $raw): self { return self::tryFrom(strtoupper($raw)) ?? self::Get; } } function describeStatus(int $code): string { return match ($code) { 200, 201, 204 成功, 301, 302, 307 重定向, 400, 401, 403 客戶端錯誤, 404 資源不存在, 500, 502, 503 服務(wù)端錯誤, default 未知狀態(tài)碼 {$code}, }; } function label(Method $m): string { return match ($m) { Method::Get 讀取, Method::Post 新建, Method::Put 更新, Method::Delete 刪除, }; } // 1. 狀態(tài)碼映射 foreach ([200, 301, 404, 500, 418] as $code) { printf(%d %s\n, $code, describeStatus($code)); } // 2. 嚴(yán)格比較的演示 $raw 1; echo match ($raw) { 1 命中整數(shù)分支\n, default 未命中整數(shù)分支因為 1 ! 1\n, }; // 3. 枚舉 match窮盡且?guī)袨?foreach ([get, POST, put, delete, patch] as $rawMethod) { $m Method::fromRaw($rawMethod); printf(%-7s %s, safe%s\n, $rawMethod, label($m), $m-isSafe() ? yes : no); } // 4. 拋異常作為臂的表達(dá)式throw 從 PHP 8.0 起也是表達(dá)式 function mustPositive(int $n): int { return match (true) { $n 0 $n, default throw new InvalidArgumentException(必須是正數(shù)實際 {$n}), }; } try { mustPositive(-5); } catch (InvalidArgumentException $e) { echo caught: , $e-getMessage(), PHP_EOL; } // 5. UnhandledMatchError 繼承自 Error必須用 Throwable 接 try { echo match (99) { 1 one, }; } catch (Exception $e) { echo catch (Exception) 接不住\n; // 不會執(zhí)行 } catch (Throwable $e) { echo caught Throwable: , get_class($e), PHP_EOL; }預(yù)期輸出200 成功 301 重定向 404 資源不存在 500 服務(wù)端錯誤 418 未知狀態(tài)碼 418 未命中整數(shù)分支因為 1 ! 1 get 讀取, safeyes POST 新建, safeno put 更新, safeno delete 刪除, safeno patch 讀取, safeyes caught: 必須是正數(shù)實際 -5 caught Throwable: UnhandledMatchError注意第 5 段的結(jié)果catch (Exception $e)那個塊不會執(zhí)行因為UnhandledMatchError繼承自Error而Error與Exception是Throwable下兩個平行的分支。這是一個非常隱蔽的坑——如果你在全局異常處理器里只寫了catch (Exception $e)match未命中導(dǎo)致的錯誤會直接繞過它變成 500。常見坑點1. 忘了default外部輸入一超范圍就崩?match ($httpCode) { 200 ok, 404 not found }—— 遇到 500 直接拋UnhandledMatchError? 取值集合不確定來自外部時必須寫default集合封閉枚舉才省略2. 用catch (Exception)兜UnhandledMatchError? 全局處理器只捕獲Exceptionmatch的錯誤穿透到框架錯誤頁 ? 捕獲Throwable或至少ErrorUnhandledMatchError繼承自Error3. 拿字符串去匹配整數(shù)分支?match ($_GET[type]) { 1 ..., 2 ... }$_GET永遠(yuǎn)是字符串穩(wěn)定落default? 先(int) $_GET[type]或在default里做兜底轉(zhuǎn)換4. 以為match會像switch一樣穿透? 寫成match ($x) { 1 a, 2 }—— 空臂不是繼續(xù)往下而是語法錯誤 ? 多個值共用一個結(jié)果時用逗號1, 2 a5. 在臂里寫多條語句?match ($x) { 1 $a 1; $b 2, }—— 臂必須是單個表達(dá)式 ? 把邏輯抽成函數(shù)或閉包1 (function () { ... })(),或直接改用if/elseif6.match(true)的條件順序?qū)懛? 把寬條件寫在前面$score 0放在$score 90之前后面所有臂都成了死代碼 ?match(true)從上往下取第一個命中的臂條件必須從窄到寬7. 把match的結(jié)果丟棄還在期待副作用?match ($x) { 1 saveA(), 2 saveB() };—— 能跑但丟掉了表達(dá)式的最大價值 ? 需要副作用時保留返回值或用ifmatch更適合輸入 → 輸出的純映射8. 在枚舉的match里加default把新增 case 的遺漏藏起來?default 未知讓新增的枚舉 case 悄悄返回錯誤結(jié)果 ? 枚舉場景省略default讓遺漏在測試時就炸出來總結(jié)關(guān)注點結(jié)論引入版本PHP 8.0標(biāo)題里的 8.1 有誤比較方式嚴(yán)格比較類型不一致就不命中窮盡性無default且無命中時拋UnhandledMatchError錯誤類型UnhandledMatchError繼承自Error只能用Throwable接分支寫法單個表達(dá)式多值用逗號throw也是合法表達(dá)式何時省略 default枚舉等封閉集合省略外部輸入必須寫match相比switch真正進(jìn)步的地方不是少寫幾個break而是把取值必須被完整覆蓋這件事變成了引擎能檢查的規(guī)則封閉集合漏分支會在運行時立刻報錯外部輸入沒兜底也會報錯。用好它的關(guān)鍵就兩條——該寫default的場景一個都不能省不該寫的場景一個都不要加以及進(jìn)match之前先把類型轉(zhuǎn)換做干凈。