前言先说一个版本事实match表达式是 PHP 8.0 引入的RFC 名称是matchexpression v2不是 PHP 8.1。标题里的版号有误本文按 8.0 讲文中代码在 8.0、8.1 一直到 8.5 上都能直接运行。这个混淆很常见大概是因为 8.1 同时带来了枚举Enum而match和枚举经常成对出现容易被记成同一批特性。从switch换到match之后最常见的三种症状是程序突然抛UnhandledMatchError而以前switch什么都不做match(1)的某个分支明明写了1却永远不命中以及用catch (Exception $e)兜底却完全接不住这个错误。这三个症状分别对应match的三个核心设计穷尽性检查、严格比较、错误继承自Error。理解这三条match就会从偶尔炸的语法糖变成能在编译期和运行期帮你抓 bug 的表达式。一、match 与 switch 的六点差异维度switchmatch本质语句statement表达式expression有返回值比较方式松散比较严格比较穿透不写break会穿透不会穿透天然隔离未命中静默跳过无default时抛UnhandledMatchError分支内容任意语句块必须是单个表达式多值分支只能靠穿透堆叠case用逗号1, 2, 3 x求值依次执行case判断只求值命中的那个臂返回值无直接返回可参与表达式只求值命中的那个臂这一条很实用match ($x) { 1 expensive(), 2 cheap() }在$x 2时不会执行expensive()而switch里如果逻辑写在外部函数调用位置就未必享受得到这种惰性。还有一条容易忽略的细节match的分支列表里逗号分隔的多个条件是或的关系而且这些条件本身是表达式可以用常量、甚至可以用比较表达式——这也是后面match(true)技巧的基础。二、严格比较带来的连锁反应match用比较意味着类型必须完全一致。这一条会以三种形式咬人?php declare(strict_types1); // 1. 字符串数字 vs 整数 echo match (1) { 1 整数一, // 不命中1 ! 1 default 未命中, }, PHP_EOL; // 输出未命中 // 2. 整数 vs 浮点 echo match (1.0) { 1 整数一, // 不命中1.0 ! 1 default 未命中, }, PHP_EOL; // 输出未命中 // 3. 与比较表达式混用时要显式转型 $raw 42; echo match (true) { (int) $raw 40 大数, default 小数, }, PHP_EOL; // 输出大数从 HTTP 请求、环境变量、配置文件里读出来的值几乎都是字符串直接丢给match用整数分支接就会稳定地落到default如果没有default就是UnhandledMatchError。规范做法是在进match之前先做一次显式类型转换把类型不确定性收敛到一个点上。三、match(true) 与枚举搭配match(true)是把match当多分支 if用的官方常见技巧每个臂写一个布尔表达式第一个为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 非法分数, }; } echo levelOf(95), levelOf(85), levelOf(72), levelOf(30), PHP_EOL; // ABCD echo levelOf(-1), PHP_EOL; // 非法分数顺序在这里极其重要match(true)是从上往下取第一个命中的臂把范围宽的判断写在前面后面就永远不会命中。上面如果把$score 0写在最前面所有分数都会变成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最值得强调的用法不要用default去掩盖遗漏。对枚举这类取值集合封闭的场景省略default是正确选择对取值来自外部的场景HTTP 状态码、第三方返回码则必须写default。四、完整可运行示例?php declare(strict_types1); // 需要 PHP 8.0match 表达式在 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, }; } /** 外部字符串 - 枚举 - 行为类型转换只在一个地方做 */ 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 服务端错误, default 未知状态码 {$code}, }; } function label(Method $m): string { return match ($m) { Method::Get 读取, Method::Post 新建, Method::Put 更新, Method::Delete 删除, }; } // 1. 状态码映射 foreach ([200, 301, 404, 500, 418] as $code) { printf(%d %s\n, $code, describeStatus($code)); } // 2. 严格比较的演示 $raw 1; echo match ($raw) { 1 命中整数分支\n, default 未命中整数分支因为 1 ! 1\n, }; // 3. 枚举 match穷尽且带行为 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. 抛异常作为臂的表达式throw 从 PHP 8.0 起也是表达式 function mustPositive(int $n): int { return match (true) { $n 0 $n, default throw new InvalidArgumentException(必须是正数实际 {$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; // 不会执行 } catch (Throwable $e) { echo caught Throwable: , get_class($e), PHP_EOL; }预期输出200 成功 301 重定向 404 资源不存在 500 服务端错误 418 未知状态码 418 未命中整数分支因为 1 ! 1 get 读取, safeyes POST 新建, safeno put 更新, safeno delete 删除, safeno patch 读取, safeyes caught: 必须是正数实际 -5 caught Throwable: UnhandledMatchError注意第 5 段的结果catch (Exception $e)那个块不会执行因为UnhandledMatchError继承自Error而Error与Exception是Throwable下两个平行的分支。这是一个非常隐蔽的坑——如果你在全局异常处理器里只写了catch (Exception $e)match未命中导致的错误会直接绕过它变成 500。常见坑点1. 忘了default外部输入一超范围就崩❌match ($httpCode) { 200 ok, 404 not found }—— 遇到 500 直接抛UnhandledMatchError✅ 取值集合不确定来自外部时必须写default集合封闭枚举才省略2. 用catch (Exception)兜UnhandledMatchError❌ 全局处理器只捕获Exceptionmatch的错误穿透到框架错误页 ✅ 捕获Throwable或至少ErrorUnhandledMatchError继承自Error3. 拿字符串去匹配整数分支❌match ($_GET[type]) { 1 ..., 2 ... }$_GET永远是字符串稳定落default✅ 先(int) $_GET[type]或在default里做兜底转换4. 以为match会像switch一样穿透❌ 写成match ($x) { 1 a, 2 }—— 空臂不是继续往下而是语法错误 ✅ 多个值共用一个结果时用逗号1, 2 a5. 在臂里写多条语句❌match ($x) { 1 $a 1; $b 2, }—— 臂必须是单个表达式 ✅ 把逻辑抽成函数或闭包1 (function () { ... })(),或直接改用if/elseif6.match(true)的条件顺序写反❌ 把宽条件写在前面$score 0放在$score 90之前后面所有臂都成了死代码 ✅match(true)从上往下取第一个命中的臂条件必须从窄到宽7. 把match的结果丢弃还在期待副作用❌match ($x) { 1 saveA(), 2 saveB() };—— 能跑但丢掉了表达式的最大价值 ✅ 需要副作用时保留返回值或用ifmatch更适合输入 → 输出的纯映射8. 在枚举的match里加default把新增 case 的遗漏藏起来❌default 未知让新增的枚举 case 悄悄返回错误结果 ✅ 枚举场景省略default让遗漏在测试时就炸出来总结关注点结论引入版本PHP 8.0标题里的 8.1 有误比较方式严格比较类型不一致就不命中穷尽性无default且无命中时抛UnhandledMatchError错误类型UnhandledMatchError继承自Error只能用Throwable接分支写法单个表达式多值用逗号throw也是合法表达式何时省略 default枚举等封闭集合省略外部输入必须写match相比switch真正进步的地方不是少写几个break而是把取值必须被完整覆盖这件事变成了引擎能检查的规则封闭集合漏分支会在运行时立刻报错外部输入没兜底也会报错。用好它的关键就两条——该写default的场景一个都不能省不该写的场景一个都不要加以及进match之前先把类型转换做干净。