你的钱去哪了?

想象一个支付系统。用户在 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] 的效果


用在结构体上:所有返回它的地方都报警

最直接的做法是把 #[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::fromVec::new,就别加了。调用方有时就是想拿个中间结果,数据没有"必须处理"一说。

内部基础设施库慎重使用。如果是你的公共 API 或者业务层代码,值得加。如果是底层库,过度使用会让调用方写很多 let _ =,反而造成噪音。

这个属性本身没有运行时开销,编译期检查完了就完了,是 Rust “让错误在编译时暴露"哲学的一个小体现。


FAQ

Q:加了 #[must_use] 后,有时候我不知道怎么处理这个返回值,怎么办?

A:先用 let _ = 接住。编译通过之后,再思考业务逻辑该怎么处理。这样至少不会漏掉,思考清楚后再改掉 let _ =

Q:结构体和函数都加了 #[must_use],会不会重复报警?

A:不会。编译器只报一次。而且函数级别的标记粒度更细,如果你的 API 设计需要区分对待,优先考虑打在函数上。

Q:枚举的某个变体想标记为 #[must_use],能做到吗?

A:不能直接对枚举变体加 #[must_use]。需要在枚举本身上加,然后通过代码逻辑区分哪些变体需要特殊处理。或者把需要处理的变体单独做成一个类型来标记。