Rust 的 #[must_use]:让你的返回值不被悄悄忽略

你的钱去哪了?
想象一个支付系统。用户在 App 点了一下"确认支付",后端扣了钱,但订单状态没更新。用户收到一条"支付成功"的推送,钱却永远卡在半空中。
这种 Bug 最讨厌的地方在哪?代码跑起来没有任何报错,一切都正常,但钱没了,订单也没了。
很多情况下,罪魁祸首是返回值被悄悄忽略了。比如这样:
fn create_transaction(tx_args: TxArgs) -> Transaction {
let id = generate_transaction_id();
Transaction::new(id, tx_args)
}
// 调用方:
create_transaction(tx_args); // 编译通过,运行时静默丢弃
调用方忘了用 let _ = 接住返回值,编译器一声不吭。结果业务逻辑断了一截,你得在生产环境里抓耳挠腮找原因。
#[must_use] 就是来解决这个问题的。它是 Rust 提供的一个小标签,贴在类型或函数上,编译器就会在你忽略它的返回值时报一个警告。
![对比:有无 #[must_use] 的效果](https://rexai.top/images/rust-must-use-attribute/rust-must-use-comparison.jpg)
用在结构体上:所有返回它的地方都报警
最直接的做法是把 #[must_use] 打在结构体定义上。这样任何函数返回这个类型的值,只要你没处理,编译器就会叫。
#[must_use = "交易创建后必须处理,不能直接丢弃"]
pub struct Transaction {
id: u64,
tx_args: TxArgs,
}
impl Transaction {
pub fn new(id: u64, tx_args: TxArgs) -> Self {
Self { id, tx_args }
}
}
现在编译器会在所有"看到了但没处理"的地方报警:
create_transaction(args); // warning: unused `Transaction` that must be used
Transaction::new(1, args); // warning: unused `Transaction` that must be used
AnotherFunctionThatReturnsATransaction(); // warning: unused `Transaction` that must be used
Transaction { id: 1, tx_args: args }; // warning: struct literal 也没用也会报警
警告信息里会带上你写的那句话,方便定位问题:
warning: unused `Transaction` that must be used
= note: 交易创建后必须处理,不能直接丢弃
调用方被迫做出选择,要么真的用起来,要么显式丢弃:
let tx = create_transaction(tx_args);
// 正常业务逻辑
let _ = create_transaction(tx_args); // 显式忽略,编译器不报警
显式 let _ = 是个不错的做法。代码读起来就告诉后来人:“这里我故意的。”
用在函数上:只管特定的调用点
把 #[must_use] 打在结构体上威力很大,但有时候你只想管住某个函数,不希望所有返回这个类型的地方都报警。
支付场景里可能有多个创建交易的方式。有些是合法的快速路径,不需要约束:
// 只有这个函数需要强制处理返回值
#[must_use = "创建的交易必须处理,不能直接丢弃"]
pub fn create_transaction(tx_args: TxArgs) -> Transaction {
let id = generate_transaction_id();
Transaction::new(id, tx_args)
}
// 这个是内部工具函数,返回值不需要调用方处理
fn build_empty_transaction() -> Transaction {
Transaction::new(0, TxArgs::default())
}
加了 #[must_use] 的函数,调用后不处理会报警。但 Transaction::new() 作为构造函数没有被标记,所以 build_empty_transaction 内部的 Transaction::new() 调用不会触发警告。
这种细粒度控制适合 API 设计:你想让用户用你指定的入口函数,而不是绕过去直接用构造函数。
用在 Trait 上:动态和静态返回都管得住
第三种用法是把 #[must_use] 打在 Trait 定义上。这样无论返回 impl Trait 还是 Box<dyn Trait>,只要调用者没处理结果,就会收到警告。
#[must_use = "支付创建后必须处理"]
pub trait Payment {
fn process(&self);
}
pub fn create_payment(amount: u64) -> impl Payment {
CreditCardPayment { amount }
}
pub fn create_dynamic_payment(amount: u64) -> Box<dyn Payment> {
Box::new(CreditCardPayment { amount })
}
两种返回形式都会被管住:
create_payment(50); // warning: unused implementer of `Payment` that must be used
create_dynamic_payment(50); // warning: unused boxed `Payment` trait object that must be used
let _ = create_payment(50); // 显式忽略,没问题
这个用法的典型场景是支付、通知、写入这类"不处理就会出问题"的操作。Trait 上加一个标记,整个抽象层的消费者都会被约束到。

标准库里就有现成的例子
你每天都在用 #[must_use],只是没注意过。
Result 的定义里就有这行:
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
所以你忽略 Result 返回值时,编译器就会报:“unused Result that must be used”。Rust 标准库把这个属性打在 Result 定义上,所有返回 Result 的函数自动获得这个保护。
Option::is_some 也有类似的设计:
#[must_use = "if you intended to assert that this has a value, consider `.unwrap()` instead"]
pub const fn is_some(&self) -> bool {
// ...
}
提醒你:如果真的想断言值存在,直接用 .unwrap() 语义更清晰。
什么时候用它
经验之谈:
业务上不能跳过的操作,比如交易、支付、写入、发送通知,打上 #[must_use]。
纯数据转换函数,比如 String::from、Vec::new,就别加了。调用方有时就是想拿个中间结果,数据没有"必须处理"一说。
内部基础设施库慎重使用。如果是你的公共 API 或者业务层代码,值得加。如果是底层库,过度使用会让调用方写很多 let _ =,反而造成噪音。
这个属性本身没有运行时开销,编译期检查完了就完了,是 Rust “让错误在编译时暴露"哲学的一个小体现。
FAQ
Q:加了 #[must_use] 后,有时候我不知道怎么处理这个返回值,怎么办?
A:先用 let _ = 接住。编译通过之后,再思考业务逻辑该怎么处理。这样至少不会漏掉,思考清楚后再改掉 let _ =。
Q:结构体和函数都加了 #[must_use],会不会重复报警?
A:不会。编译器只报一次。而且函数级别的标记粒度更细,如果你的 API 设计需要区分对待,优先考虑打在函数上。
Q:枚举的某个变体想标记为 #[must_use],能做到吗?
A:不能直接对枚举变体加 #[must_use]。需要在枚举本身上加,然后通过代码逻辑区分哪些变体需要特殊处理。或者把需要处理的变体单独做成一个类型来标记。