首页 / 资讯中心 / 文章详情

PHP 8.2的never类型怎么用才规范

PHP 8.2的never类型怎么用才规范 ★ FEATURED ARTICLE
前言先纠正版本never返回类型是 PHP 8.1 引入的不是 PHP 8.2。标题里的版号有误本文按 PHP 8.1 讲。之所以经常被记成 8.2大概是因为 PHP 8.2 引入了readonly类和 DNF 类型(AB)|C这几个特性在同期文章里总是一起出现。典型的误用症状是这样你写了一个abort()或者fail()函数它永远只做一件事——抛异常。为了让签名看起来「干净」你给它标了: void。于是静态分析工具PHPStan、Psalm和 IDE 都认为这个函数会正常返回函数调用点之后的代码被视为「可达」一大堆积压的类型告警和「可能返回 null」的提示全都消失不了更糟的是你明明写了$user findOrFail($id);后面直接$user-getName()工具仍然认为$user可能是null。第二个常见症状是运行时的某个函数在if分支里抛异常函数末尾又忘了补一句抛异常声明了never之后直接抛TypeError: ... never-returning function must not return。这个报错信息看着很吓人其实意思很简单函数执行到了末尾而never承诺了「不会有任何返回」。本文讲清never的语义、它和void的本质区别、协变规则以及写规范的关键约束。一、never 是什么底类型Bottom Typenever是一个类型但它没有任何值。它的含义是「这个函数永远不会正常返回」——要么抛异常要么exit要么陷入死循环总之控制流不会回到调用点。在类型论里never是底类型bottom type它是所有类型的子类型。这带来一个非常实用的推论——任何需要返回值的地方你都可以调用一个never函数?php // PHP 8.1 function fail(string $msg): never { throw new \RuntimeException($msg); } // $id 声明是 int但这里可以放心fail() 不会返回 $id is_numeric($raw) ? (int) $raw : fail(id 必须是数字);fail()的返回值类型是never而never是int的子类型所以整个三元表达式的类型仍然是int静态分析器完全接受。这就是void做不到的事void函数「有返回只是没值」你无法把它放进一个表达式。对比项voidnever引入版本PHP 7.1PHP 8.1允许出现在返回类型返回类型可以出现在表达式里不行void无值可以是任何类型的子类型函数体走到底部合法隐式返回null非法抛TypeError函数体里有return;合法非法编译期 Fatal error返回值无无控制流不返回静态分析效果后续代码可达后续代码不可达类型收窄二、写规范的三条硬约束约束不是风格建议是引擎强制的约束一函数体内不能出现任何return。包括不带值的return;。这一条是编译期检查的?php // PHP 8.1 —— 编译就报 Fatal error: A never-returning function must not return function nope(): never { return; }约束二函数不能执行到末尾。这一条是运行期检查的。只有当控制流真的走到函数末尾时才会抛TypeError?php // PHP 8.1 —— 声明合法但某个分支走到底就会抛 TypeError function maybeFail(int $x): never { if ($x 0) { throw new \InvalidArgumentException(负数); } // $x 0 时走到这里 → TypeError }这类 bug 的隐蔽之处在于测试里只跑了-1的用例一切正常线上出现正数输入时才炸。约束三never只能用在返回类型位置。参数类型、属性类型、类常量类型都不接受never。同时never函数里不能出现yield——生成器函数Generator的返回类型必须是Generator的超类型声明never会在编译期被拒绝。写「永不返回」的异步/迭代逻辑时别指望把never和生成器混在一起。三、协变父类方法可以被收窄成 neverPHP 的返回类型支持协变Covariance子类可以把返回类型收窄成更具体的类型。never是最窄的类型所以?php // PHP 8.1 interface Finder { public function find(int $id): ?object; } class ThrowingFinder implements Finder { // 合法返回类型从 ?object 收窄为 never public function find(int $id): never { throw new \OutOfBoundsException(找不到 $id); } }反过来不成立父类声明never、子类声明void或具体类型是放宽返回类型会直接报兼容性错误。规格上的直觉是「子类承诺的不能比父类少」——父类说「我不会返回」子类就必须也不会返回。这条规则在接口设计里很有用接口声明一个可能返回值的类型实现类可以用never表示「我这个实现只会抛异常」。这比在每个实现里写throw然后标void精确得多也让调用方的类型推导保持准确。四、完整可运行示例下面需要PHP 8.1。脚本演示三种合法的never场景抛异常、exit、无限循环不会返回、契约收窄以及越界时的运行时报错。?php declare(strict_types1); // PHP 8.1 /** 永远抛异常可用于任何需要值的位置 */ function fail(string $msg): never { throw new \RuntimeException($msg); } /** 永远退出CLI 里用于提前结束 */ function abort(string $msg): never { fwrite(STDERR, [abort] $msg\n); exit(1); } interface Loader { public function load(string $key): string; } /** 用 never 表达「这个实现只可能抛异常」 */ class FailingLoader implements Loader { public function load(string $key): never { throw new \LogicException(未实现: $key); } } // 1) never 作为表达式的一部分 $raw 42; $id is_numeric($raw) ? (int) $raw : fail(id 必须是数字); echo id $id\n; $raw abc; try { $id is_numeric($raw) ? (int) $raw : fail(id 必须是数字); echo 这行永远不会执行\n; } catch (\RuntimeException $e) { echo 捕获: , $e-getMessage(), PHP_EOL; } // 2) 契约收窄 $loader new FailingLoader(); try { $loader-load(config); } catch (\LogicException $e) { echo 捕获: , $e-getMessage(), PHP_EOL; } // 3) 越界声明 never 但走到了函数末尾 function broken(int $x): never { if ($x 0) { throw new \InvalidArgumentException(负数); } } try { broken(1); } catch (\TypeError $e) { echo TypeError: , $e-getMessage(), PHP_EOL; } // 4) 反射确认返回类型名 $rt (new ReflectionMethod(broken))-getReturnType(); echo 返回类型: , $rt-getName(), PHP_EOL;运行输出id 42 捕获: id 必须是数字 捕获: 未实现: config TypeError: broken(): never-returning function must not return 返回类型: never在真实项目里never最有价值的地方是配合「或失败」语义的函数命名约定findOrFail()、assertValid()、redirect()、abort()。这些函数只要标上neverIDE 立刻不再把调用点之后的代码标黄if ($x null) { fail(); } $x-foo();也不会再有「可能为 null」的告警。常见坑点坑 1用void表达「一定会抛异常」。❌function fail(string $m): void { throw new \Exception($m); }——静态分析器和 IDE 都不知道后面的代码不可达类型推导全部失效。 ✅ 改成: never让工具理解控制流。坑 2在never函数里写return;想提前结束。❌return;是编译期致命错误A never-returning function must not return整份文件都加载不了。 ✅ 要提前结束就用throw或者干脆把函数声明改成void。坑 3所有分支里都写throw但漏了兜底分支。❌ 声明never却在if之外走到函数末尾运行时抛TypeError测试用例容易覆盖不到。 ✅ 让函数结构保证「任何路径都抛」例如把逻辑包进try/catch后在catch里重新抛或在末尾加一个throw new \LogicException(unreachable)。坑 4把异常吞掉了却还声明never。❌try { throw new \Exception(); } catch (\Exception $e) { /* 记个日志 */ }—— 异常被吃掉函数正常返回never立刻被破坏。 ✅ 要么在catch里重新抛出throw $e;要么不要声明never。坑 5以为never能用在参数或属性上。❌function f(never $x)、private never $v;都是无效声明报错信息指向类型关键字本身容易误以为是命名空间问题。 ✅never只写在返回值位置参数要表达「永远不传值」在语言层面根本没有对应写法。坑 6把never函数和生成器混用。❌ 一个含yield的函数声明: never编译期就会因为返回类型与Generator不兼容而失败。 ✅ 生成器老老实实写: Generator或: iterable需要「永不返回」就在生成器内部抛异常。坑 7以为收紧返回类型会破坏 LSP。❌ 有人担心父类声明?object、子类改成never会违反里氏替换原则于是改成void并到处补throw。 ✅ 返回类型协变是允许的收窄成never完全合法且更精确被禁止的是反向放宽。坑 8用never标注带exit的函数却在 CLI 与 FPM 里行为不同。❌ 同一份代码在 FPM 里exit会触发响应结束流程在 CLI 里直接终止脚本测试时表现不一致容易误判成never没生效。 ✅ 把「永远退出」和「永远抛异常」拆成两个函数业务逻辑依赖抛异常的那个退出逻辑只放在入口处。总结关注点结论引入版本PHP 8.1标题写 8.2 是错的语义底类型是所有类型的子类型表示控制流永不返回允许位置仅返回类型参数、属性都不允许编译期检查出现任何return含return;→ Fatal error运行期检查执行到函数末尾 →TypeError与void区别void有返回无值、可用于无返回值分支never不可达、可放进表达式继承规则子类可把返回类型收窄为never反之不行典型场景fail()/abort()/findOrFail()/ 重定向函数never的价值不在运行时而在静态分析和可读性它把「这个函数不会回来」从注释里的口头约定变成了引擎和工具都认得的契约。写规范的关键只有三点——不在里面写return、保证每条路径都抛异常或退出、别把它放到返回类型以外的位置。做到这三点never就能帮你把一大类「可能为 null」「不可达代码」的告警一次性消掉。
阅读完成 · 觉得有帮助?
咨询建站