PHP 8.5 #[\NoDiscard] 揪出“忽略返回值“的 Bug

作者:catchadmin日期:2026/1/9

PHP 8.5 #[\NoDiscard] 揪出"忽略返回值"的 Bug

有些 bug 会导致异常、致命错误、监控面板一片红。

还有一类 bug 长这样:“一切都跑了,但什么都没发生”。方法调了,副作用也有了,但关键返回值(成功标志、错误列表、新的不可变实例)被扔掉了。粗看代码没毛病,测试没覆盖到边界情况也能过。bug 就这么混进生产环境。

PHP 一直允许这种风格的失误:

1doSomethingImportant(); // 返回了一个值……但没人用
2

PHP 8.5 新增了一种原生方式来标记这类情况:#[\NoDiscard]

给函数或方法加上 #[\NoDiscard],调用方要是不用返回值,PHP 就会发警告。可以把它理解为"编译器级别的提示"(实际由引擎在运行时/编译时执行),不抛异常、不改行为,只是让 API 更安全。

本文讲的是怎么用好 #[\NoDiscard]

  • 它能防哪些 bug
  • 具体语义(什么算"用了"返回值)
  • 高价值模式:Result / Either、不可变构建器
  • 怎么推广才不会让团队反感
  • 什么时候别用(误报确实存在)

原文 PHP 8.5 #[\NoDiscard] 揪出"忽略返回值"的 Bug

常见"惯犯":返回值被忽略的几种场景

PHP 代码库里有几个常见的"惯犯"。

返回布尔值,默认它总是成功

典型案例:

1$ok = rename($tmpFile, $finalFile);
2

有人重构,赋值没了:

1rename($tmpFile, $finalFile);
2// 继续跑,当作移动成功了
3

开发环境没事。生产环境碰上权限边界情况,你读的文件压根没移动过。

不是每个返回布尔值的函数都该标 #[\NoDiscard]。但你自己的 API 里,如果返回值有意义,忽略它至少该引起警觉。

返回错误信息,但正常路径太常见,失败没人注意

批处理是重灾区:99.9% 成功,忽略返回值不会破坏大多数运行。

典型场景:

  • 函数处理多个条目
  • 返回每个条目的错误详情
  • 副作用照常发生
  • 忽略返回值 = 部分失败被藏起来了

官方 RFC 就是用这个逻辑来解释 #[\NoDiscard] 的设计动机。

不可变 API 返回新实例,调用却像在原地改

这种情况很微妙,从可变对象迁移到不可变对象时特别常见。

你写了个不可变的"更新"方法:

1$user = $user->withEmail($newEmail);
2

后来有人写成:

1$user->withEmail($newEmail);
2// 以为 $user 变了……其实没变
3

没报错,没异常,状态就是静悄悄地没变。

RFC 明确提到 DateTimeImmutable::set*() 就是典型:“听起来像原地修改,实际返回新实例”。

返回 Result 对象,但忘了解包检查

如果你用 Result 类型(或 Either)来避免异常,忽略返回值基本就是忽略了错误。

不一定马上出问题——但错误处理被推到了"以后再说",而"以后"往往等于"永远不"。

#[\NoDiscard] 是什么

简单说,#[\NoDiscard] 是加在函数和方法上的属性,意思是:

“调用了却不用返回值?多半是 bug。”

最简用法:

1#[\NoDiscard]
2function createSession(): string {
3    return bin2hex(random_bytes(16));
4}
5
6createSession(); // PHP 8.5 中会产生警告
7

RFC 和 PHP 手册里定义了具体行为:

  • 调用 #[\NoDiscard] 函数但没用返回值,PHP 发警告
  • 内置函数发 E_WARNING;用户定义函数发 E_USER_WARNING
  • 可以带消息:#[\NoDiscard("…")],消息会出现在警告里(跟 #[Deprecated] 类似)

什么算"用了"?

最关键的细节:"用了"是语法层面的判断,不是语义层面的。

RFC 对"用了返回值"的定义很宽松:返回值只要成为任意表达式的一部分就行。赋值给变量——哪怕是个哑变量——算。类型转换也算。

所以下面这些都算"用了":

1$unusedButAssigned = createSession(); // 无警告
2(bool) createSession();               // 无警告(但见下面关于 OPcache 的说明)
3

也就是说 #[\NoDiscard] 不保证行为正确,只保证你没把结果直接扔地上。

(void) 强制转换:显式丢弃

PHP 8.5 还引入了 (void) 强制转换:

1(void) createSession(); // 无警告
2

没有运行时效果,纯粹表明意图:"是的,我故意不用它。"可以用来抑制 #[\NoDiscard] 警告,IDE 和静态分析工具也能识别。

RFC 里有个细节:(void) 是语句不是表达式,不能嵌到其他表达式里,否则语法错误。

使用约束

RFC 规定这些情况会编译报错:

  • 返回类型是 : void: never 的函数
  • 必须是 void / 无返回的魔术方法(__construct__clone 等)
  • 属性钩子

所以这样写会报错:

1#[\NoDiscard]
2function logSomething(string $msg): void {
3    error_log($msg);
4}
5// Fatal: void 函数不返回值,但 #[\NoDiscard] 要求有返回值
6

这是故意的:没东西可丢弃,这个属性就没意义。

锐边:警告可能变致命错误

大多数团队把警告当噪音。有些团队把警告转成异常(严格环境里常见)。

RFC 指出,引擎在调用函数之前(参数求值之后)就验证"返回值有没有被用"。如果你配了个会抛异常的错误处理器,警告一触发就抛异常,函数压根不会被调用——RFC 把这叫"fail-closed"行为。

#[\NoDiscard] 函数来说,这通常是好事(忽略返回值本来就不安全),但如果函数有重要副作用,你得心里有数。

实用示例

看看实际怎么用。下面这些模式是 #[\NoDiscard] 真正能发挥价值的地方。

Result 类型

一个最小化的 Result 实现:

1<?php
2declare(strict_types=1);
3
4final class Result
5{
6    private function __construct(
7        private bool $ok,
8        private mixed $value,
9        private ?string $error,
10    ) {}
11
12    public static function ok(mixed $value = null): self
13    {
14        return new self(true, $value, null);
15    }
16
17    public static function err(string $error): self
18    {
19        return new self(false, null, $error);
20    }
21
22    public function isOk(): bool { return $this->ok; }
23    public function isErr(): bool { return !$this->ok; }
24
25    public function unwrap(): mixed
26    {
27        if (!$this->ok) {
28            throw new RuntimeException($this->error ?? 'Unknown error');
29        }
30        return $this->value;
31    }
32
33    public function error(): ?string { return $this->error; }
34}
35

假设有个验证函数返回 Result:

1#[\NoDiscard("Validation results must be handled (ok/err)")]
2function validateUsername(string $name): Result
3{
4    $name = trim($name);
5    if ($name === '') {
6        return Result::err("Username cannot be empty.");
7    }
8    if (strlen($name) < 3) {
9        return Result::err("Username is too short.");
10    }
11    return Result::ok($name);
12}
13

这样调用会触发警告:

1validateUsername($_POST['username'] ?? '');
2

这就是你想要的效果:用了 Result 模式,忽略它几乎肯定是写错了。

正确的写法变成显式的:

1$res = validateUsername($_POST['username'] ?? '');
2if ($res->isErr()) {
3    http_response_code(422);
4    echo $res->error();
5    exit;
6}
7$username = $res->unwrap();
8

开发者还能写 $_ = validateUsername(...) 然后不管它吗?能,PHP 会认为"用了"。但主要的失败模式——不小心写了裸调用——被拦住了。

Either 风格

有些团队喜欢更结构化的错误:

1final class ValidationError
2{
3    public function __construct(public string $code, public string $message) {}
4}
5
6final class Either
7{
8    private function __construct(
9        public bool $isRight,
10        public mixed $right,
11        public ?ValidationError $left,
12    ) {}
13
14    public static function right(mixed $value): self
15    {
16        return new self(true, $value, null);
17    }
18
19    public static function left(ValidationError $err): self
20    {
21        return new self(false, null, $err);
22    }
23}
24

返回 Either 的函数标 #[\NoDiscard] 通常没问题,因为本来就是要强制调用方决定走哪个分支。

不可变构建器

假设有个不可变构建器,每个方法返回新的构建器:

1<?php
2declare(strict_types=1);
3
4final readonly class InvoiceBuilder
5{
6    public function __construct(
7        public array $lines = [],
8        public int $totalCents = 0,
9    ) {}
10
11    #[\NoDiscard("InvoiceBuilder is immutable; you must capture the returned builder.")]
12    public function withLine(string $label, int $amountCents): self
13    {
14        if ($amountCents < 0) {
15            throw new InvalidArgumentException('amountCents must be >= 0');
16        }
17        $newLines = $this->lines;
18        $newLines[] = ['label' => $label, 'amountCents' => $amountCents];
19        return new self(
20            lines: $newLines,
21            totalCents: $this->totalCents + $amountCents
22        );
23    }
24
25    #[\NoDiscard("Calling build() without using the invoice is almost certainly a bug.")]
26    public function build(): array
27    {
28        return [
29            'lines' => $this->lines,
30            'totalCents' => $this->totalCents,
31        ];
32    }
33}
34

看看典型错误:

1$builder = new InvoiceBuilder();
2$builder->withLine('Subscription', 1500);
3$builder->withLine('Support', 500);
4$invoice = $builder->build();
5

没有 #[\NoDiscard],这会产生一张没有行项目的发票,因为返回的构建器被扔掉了。

加上 #[\NoDiscard],每个被忽略的 withLine() 返回都会触发警告,逼你写对:

1$builder = (new InvoiceBuilder())
2    ->withLine('Subscription', 1500)
3    ->withLine('Support', 500);
4$invoice = $builder->build();
5

这就是 #[\NoDiscard] 要揪出来的 bug:容易犯、测试常能过、生产环境让人头疼。

PHP 自己也在用

RFC 给一小部分原生 API 加了 #[\NoDiscard],这些 API 忽略结果容易出隐蔽问题:

  • flock()(忽略锁定失败,竞争条件下可能数据损坏)
  • DateTimeImmutable::set*()(从可变 DateTime 迁移过来时的常见坑)

就算你从不直接用这些函数,这也说明一件事:这个特性针对的是真实场景里的错误,不是为了理论上的"纯粹"。

采用策略

到处加 #[\NoDiscard] 只会制造噪音,团队迟早习惯性忽略。RFC 的建议是:只在忽略返回值可能是无意的、且会导致测试期间难以发现的 bug 的地方用。

务实的推广思路:

从危险的地方开始

高价值场景:

  • 可能部分失败但继续执行的领域操作(批处理)
  • 返回 Result / 错误列表的持久化调用
  • 不可变更新方法(不可变对象上的 with*set*
  • 用返回值表示失败的 “try” 风格 API

低价值场景:

  • 纯函数(如 str_contains() 这类检查):调用后什么都不做本来就奇怪,忽略返回值很少造成隐蔽问题。RFC 拿 str_contains() 当反面例子
  • 主要靠副作用、返回值只是顺便给的方法

让警告可见,但别炸生产

引擎发的是警告(不是异常),所以可以逐步收紧:

  • 开发环境:把警告喊出来
  • CI:把 E_USER_WARNING 当失败(可选,过渡一段时间后)
  • 生产环境:保持默认,除非你确定警告策略够严格

记住:如果你们把警告转成异常,#[\NoDiscard] 会直接阻止函数运行(fail-closed)。有时候这是好事,但这种行为变化得有意识地引入。

代码审查集成

#[\NoDiscard] 用来强化团队规则效果最好,不是用来代替思考的。

下面是一套跟代码审查配合良好的简单规则:

把警告当设计信号

看到 #[\NoDiscard] 警告,别急着"消掉它"。先问:

  • 是不小心忽略的吗?(最常见)
  • 如果确实想忽略,用 (void) 合适吗?
  • 还是说 API 返回的东西本身就有问题?

用 (void) 表明意图

比如:调用一个返回缓存键的方法,但你只要副作用:

1(void) $cache->warmUp($userId);
2

这是干净、可读的约定:我故意丢弃这个值。

比下面这种写法强多了:

1$unused = $cache->warmUp($userId);
2

因为 $unused 重构后很容易留下来,让后面的人摸不着头脑。

跟静态分析配合

静态分析器和 IDE 本来就会对纯函数的未使用返回值报警。RFC 提到,PHPStorm、PHPStan、Psalm 这些工具已经能抓"纯返回值被忽略"的问题(比如 DateTimeImmutable 的坑),但它们一般没法覆盖非纯函数的重要返回值——#[\NoDiscard] 填的就是这个空白。

所以组合起来是这样:

  • 静态分析器:“纯返回值没用”
  • #[\NoDiscard]:“重要返回值没用”(哪怕是非纯的)

重构示例

做个贴近真实场景的重构:一个"保存"并返回状态的方法。

之前

1final class UserRepository
2{
3    public function save(User $user): bool
4    {
5        // ... 写入数据库 ...
6        // 冲突/失败时返回 false
7        return true;
8    }
9}
10

调用点往往变成:

1$repo->save($user);
2// 当作保存成功了
3

如果返回值有意义,这就是埋着的雷。

之后

1final class UserRepository
2{
3    #[\NoDiscard("Save may fail; handle the return value or explicitly discard it with (void).")]
4    public function save(User $user): bool
5    {
6        // ... 写入数据库 ...
7        return true;
8    }
9}
10

现在,忽略它的调用点都会报警告。

更好的做法

布尔值描述性不够。能改就返回 Result:

1final class UserRepository
2{
3    #[\NoDiscard("Save may fail; callers must handle the Result.")]
4    public function save(User $user): Result
5    {
6        // 示例逻辑
7        $ok = true;
8        if (!$ok) {
9            return Result::err("Write failed due to conflict.");
10        }
11        return Result::ok($user);
12    }
13}
14

现在更难不小心跳过错误处理,API 也更自文档化。

确实想忽略时

有些场景确实要忽略:

  • best-effort 的缓存写入
  • 遥测发送
  • 顺手清理

这时候 (void) 正合适:

1(void) $repo->save($user); // "我就是不想检查这个。"
2

代码审查看着清楚,也能防止无意中"静默忽略"又混回来。

限制和误报

#[\NoDiscard] 很强大,但不是通用的"质量徽章"。用多了就是噪音,噪音会淹没信号。

别用在无害的函数上

纯查询比如:

  • str_contains()
  • strlen()
  • 字符串转换辅助函数

调用了却什么都不做,bug 本来就很明显:算了个东西没用,还没副作用。RFC 明确说不要在 str_contains() 这类函数上用 #[\NoDiscard],因为忽略结果本来就不太可能,而且除了浪费点计算没啥坏处。

别用来强制编码风格

你会想给很多方法标 #[\NoDiscard],理由是"调用者总是接返回值更干净"。这是风格偏好,不是安全问题。

只在忽略返回值可能是无意的、而且有害的地方用。

注意"发射后不管"的 API

有些方法返回值只是顺便给的,主要靠副作用。给它们加 #[\NoDiscard] 会逼调用者到处写 (void),换一种杂乱而已。

如果你发现某个函数有一堆 (void),说明属性可能加错地方了。

“用了"不等于"处理了”

因为"用了"的定义很宽松,你可以满足 #[\NoDiscard] 但实际上什么都没处理:

1$tmp = $repo->save($user); // 无警告,但语义上还是忽略了
2

这不是特性的缺陷——它提醒我们 #[\NoDiscard] 是护栏,不是完整的正确性证明。

团队命名约定

好团队不会只靠属性。他们用命名约定,在警告出现之前就引导正确用法。

下面是跟 #[\NoDiscard] 配合良好的命名约定:

with* 不可变更新

如果你的类是不可变的:

  • withEmail()
  • withStatus()
  • withTimeout()

这些方法基本都该标 #[\NoDiscard],因为忽略返回值通常意味着"啥也没变"。

try* 失败编码在返回值里

比如:

  • tryLock() : bool
  • tryParse() : Result
  • tryConnect() : Result

方法名以 try 开头,调用者一般会期望检查结果。标 #[\NoDiscard] 强化这个预期。

build() / finalize() 模式

build() 产生你要的东西,调用了却不用,基本就是写错了。

这里很适合加 #[\NoDiscard]

消息要简短有行动指向

好的消息是你希望队友在 CI 日志里看到的:

  • “This Result must be handled.”
  • “Immutable update: capture the returned instance.”
  • “Operation can partially fail; consume the error list.”

RFC 支持可选消息,会出现在警告文本里。

结论

支持 #[\NoDiscard] 的最强理由不是理论,是可维护性。

忽略返回值是真实 PHP 代码里反复出现的失败模式——尤其是:

  • 函数大多数时候都成功
  • API 是不可变的(返回新实例)
  • 失败靠返回值而不是异常来报告

PHP 8.5 给了一种原生手段来尽早抓住这些错误,用警告加显式 (void) 来保持故意丢弃时的可读性。

精准地用它:

  • 从忽略返回值有害的领域/服务 API 入手
  • 避开纯函数和"主要靠副作用"的方法
  • 配合命名约定(with*try*),让代码在引擎报警之前就能读出正确用法

做到这些,#[\NoDiscard] 就会成为那种安静地减少生产事故的小特性——不用逼整个团队换编程模型。

参考资料

  • PHP 8.5 发布公告(#[\NoDiscard] 概述、警告行为)
  • PHP 手册:PHP 8.5 新特性(#[\NoDiscard] 和 (void) 强制转换)
  • PHP RFC:“Marking return values as important (#[\NoDiscard])”(警告级别、"使用"的含义、(void) 强制转换细节、约束、推荐用法)

PHP 8.5 #[\NoDiscard] 揪出“忽略返回值“的 Bug》 是转载文章,点击查看原文


相关推荐


React 从入门到出门第一章 JSX 增强特性与函数组件入门
怕浪猫2026/1/1

今天咱们从 React 19 的基础语法入手,聊聊 JSX 增强特性和函数组件的核心用法。对于刚接触 React 19 的同学来说,这两块是搭建应用的基石——函数组件是 React 19 的核心载体,而 JSX 则让我们能以更直观的方式描述 UI 结构。 更重要的是,React 19 对 JSX 做了不少实用增强,比如支持多根节点默认不包裹、改进碎片语法等,这些特性能直接提升我们的开发效率。下面咱们结合具体案例,从“是什么→怎么用→为什么”三个维度,把这些知识点讲透~ 一、先搞懂核心概念:函数组


数据挖掘12
upper20202025/12/22

数据挖掘12 – 零样本分类 一、预备知识 1.底层特征(Low-level Features) 底层特征是从原始输入数据中直接提取的、最基础的、通常不具有明确语义含义的数值或信号特征。 例子(以图像为例): 像素强度(灰度值、RGB值) 2.中层属性(Mid-level Attributes / Mid-level Features) 中层属性是在底层特征基础上进一步组合、聚合或抽象得到的具有一定结构或局部语义的特征。它们比底层特征更接近人类可理解的概念,但尚未达到高层语义(如“猫”、“汽车”


JConsole 中 GC 时间统计的含义
千百元2025/12/14

要理解 JConsole 中 GC 时间统计的含义,需结合 垃圾收集器类型​ 和 统计维度​ 拆解: 1. 关于 PS MarkSweep 上的 12.575 秒 (16 收集) PS MarkSweep:是 JVM 中用于清理 老年代(PS Old Gen)​ 的垃圾收集器(属于 Full GC 收集器,触发时会暂停所有应用线程,即 STW)。 16 收集:表示该收集器 总共执行了 16 次 Full GC。 12.575 秒:这 16 次 Full GC 的 总耗


程序员从大厂回重庆工作一年
uzong2025/12/6

从大厂裸辞回重庆工作,整整一年了。 时间快得让人心惊。停下回望,从裸辞、归乡、求职到适应,再到角色转换,种种心绪,感慨颇多。 一、离开时,那句话成了种子 最后一个工作日的下午,领导把我叫到楼道,做了一次临别交谈。 他有一句话,我至今记得清清楚楚:“以后出去,一定要想办法走向管理岗位,那是完全不同的竞争力。” 当时只是记下。一年后的今天,当我开始带领一个小团队时,这句话突然在心里发了芽。 它像一颗提前埋下的种子,在合适的时节悄然生长。 二、裸辞回渝:一场恰如其分的“任性” 回重庆是裸辞的。所有


Python微服务架构在分布式电商系统中的高性能设计与实战经验总结分享
2501_941810832025/11/28

在大型电商系统中,用户请求量巨大、数据访问密集、服务链路复杂,要求系统具备高响应速度、高并发吞吐能力与稳定扩展性。Python 凭借开发效率高、生态完善与易维护特性,越来越多被用于电商系统的接口层、交易逻辑层、库存管理、推荐系统以及风控服务。本文结合实战电商系统落地经验,分享 Python 在分布式微服务架构中的模块划分、性能调优、服务治理与高并发优化,为开发者提供可落地的架构经验参考。 一、Python 架构选型思路 在传统单体架构中,全站服务聚合在同一进程中,随着并发量增长,性能和可维


Vercel React 最佳实践 中文版
ssshooter2026/1/17

React 最佳实践 版本 1.0.0 Vercel 工程团队 2026年1月 注意: 本文档主要供 Agent 和 LLM 在 Vercel 维护、生成或重构 React 及 Next.js 代码库时遵循。人类开发者也会发现其对于保持一致性和自动化优化非常有帮助。 摘要 这是一份针对 React 和 Next.js 应用程序的综合性能优化指南,专为 AI Agent 和 LLM 设计。包含 8 个类别的 40 多条规则,按影响力从关键(消除瀑布流、减少打包体积)到增量(高级模式)排序。每


墨梅博客 1.2.0 发布与 AI 开发实践 | 2026 年第 4 周草梅周报
草梅友仁2026/1/25

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。 前言 欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。 开源动态 本周依旧在开发 墨梅 (Momei) 中。 您可以前往 Demo 站试用:demo.momei.app/ 您可以通过邮箱 admin@example.com,密码momei123456登录演示用管理

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客