Rust 中的进行中工作
摘要
本文介绍了一种在 Rust 开发过程中延迟错误处理的技术和库,允许开发者临时将错误降级为警告,从而在保持正确性的同时维持生产力。
<p><a href="https://lobste.rs/s/qu1bwq/work_progress_rust">评论</a></p>
查看缓存全文
缓存时间: 2026/07/05 18:33
# 工作中的 Rust 代码
来源:https://blog.dureuill.net/articles/wip/
Rust 是一门注重正确性的可靠语言。然而,在开发过程中,为每一个可能的错误路径立即编写处理逻辑可能会成为一种负担。
本文介绍几种推迟处理正确性问题的技巧,这样你就能在快乐路径上停留更久,同时我也会介绍一个为此过程提供便利的库。
---
> 🌱 本文 100% 由手工创作,未使用生成式 AI 工具,虽稍显仓促,但请以我留下的拼写错误和不通顺的句子为证。
对于正在处理复杂逻辑的 Rust 开发者来说,还有什么比 `rustc` 突然跳出个错误更让人恼火的呢?无论是你还没返回 `Result`,还是没处理某个错误情况,或是没实现某个边界条件,编译错误的成因多种多样,都可能让你偏离原本想实现的目标。
当这种情况发生时,你通常面临两个选择:
1. 接受打断,立即实现完美的错误处理。通常你需要在 PR 准备好之前反复重构多次,这让整个过程变得比必要的更加沉重。
2. 给错误贴个“TODO 稍后处理”标签,将其降级为警告,然后继续当下的工作。
本文介绍一些在 Rust 中实现后一种选择的技巧:将即时错误降级为警告,留待以后处理。
## 为什么你需要警告?(https://blog.dureuill.net/articles/wip/#why-do-you-want-warnings)
警告在编译交互的可能性范围中占据了一个完美位置:
- 错误会阻碍其他编译步骤(生命周期等)以及测试,妨碍常见工作流程:1. 编写 WIP 代码 2. 测试 3. 将 WIP 代码转为生产代码。任何错误都会阻止从 (1) 到 (2) 的步骤。
- 另一方面,完全压制任何编译消息,则有可能会忘记问题,使其潜入生产环境 😬。
- 警告不会阻碍开发,但需要在合并 PR 之前清理干净。在 Meilisearch,我们有一个 CI,使用 `-D warning` 标志运行编译器,这会在 CI 时将所有警告变成错误,确保我们不会不小心发布 WIP 代码。
## 你为什么不直接读代码???(https://blog.dureuill.net/articles/wip/#why-don-t-you-just-read-the-f-king-code)
当然,**我会读**。阅读代码(你自己的以及同事的)是防止 bug 的绝佳屏障(https://entropicthoughts.com/code-reviews-do-find-bugs),而代码审查是在团队中有效分享知识的必要条件。
文章中讨论的所有技巧都是**额外**的方法,用于在专业规模下生成代码时,为正确性提供更多激励和更强的护栏。它们能提高质量,但不会取代基本的最佳实践。
根据我作为软件工程师的经验,任何依赖人类永不犯错的事情最终都会失败。
闲话少叙,以下是一些在原生 Rust 中实现错误推迟策略的技巧。
## 原生 Rust 的技巧(https://blog.dureuill.net/articles/wip/#techniques-with-standard-rust)
## 到处 `unwrap`(https://blog.dureuill.net/articles/wip/#unwrap-everywhere)
尽管 Rust 为我们提供了正确处理错误的结构,但这并不意味着我们必须立即处理它们。
一种常见的推迟错误处理的模式是直接对 `Result` 或 `Option` 调用 `unwrap`,以在错误路径上以 panic 为代价获取内部的值。
然后,当其余代码准备就绪时,你可以回到你的更改中,有选择地移除那些作为临时措施添加的 `unwrap`,替换为适当的错误处理。
```
fn does_not_return_result_yet(some_params: SomeType) -> SomeReturnType {
let converted = some_params.try_into().unwrap() // <- 可失败的转换,暂时忽略
converted.try_foo().unwrap() // <- 可失败的操作,暂时忽略
}
```
## 到处 `clone`(https://blog.dureuill.net/articles/wip/#clone-everywhere)
所有权是一个类似的问题,虽然稍有不同,且远不如错误处理常见。正确维护所有权有时需要修改设计,否则会出现生命周期问题。在这种情况下,一种常见的缓解方式是对值进行克隆,而不是到处移动它。
推迟解决这类问题稍微复杂一些,因为修复可能涉及以非平凡的方式更改代码的架构。不过,凭借经验,可以识别出那些以后可以通过在更高栈层传递适当变量来移除克隆的早期情况。
## `todo!` 宏(https://blog.dureuill.net/articles/wip/#the-todo-macro)
Rust 标准库提供了一个 `todo!()` 宏(https://doc.rust-lang.org/std/macro.todo.html),其用途描述如下:
> 表示未完成的代码。这在原型设计时,或者只想创建一个占位符以使代码通过类型分析时非常有用。
这确实在定义接口时极其有用,你需要提供一些函数定义,但还不需要提供可工作的实现。
因为 `todo!` 会发散(导致 panic),编译器能够将其用作大多数类型的占位表达式,所以文档中提到了类型分析。大量使用 `todo` 可以推迟一些代码路径(而不仅仅是错误)的实现,留到以后再完成。
### 题外话:不要与 `unimplemented!()` 混淆(https://blog.dureuill.net/articles/wip/#digression-not-to-be-confused-with-unimplemented)
我至少见过一次有人在互联网上犯错(https://xkcd.com/386)关于 `todo!()`:他们声称它的目的是向用户传达程序的某些功能尚未实现。
实际上,Rust 有一个专门的宏用于这个用例,它的名字是 `unimplemented!()`(https://doc.rust-lang.org/std/macro.unimplemented.html)。
`todo!()` 有不同的受众:代码的作者及其审查者。没有 `todo` 应该进入生产环境,它只是 WIP Rust 的一个便利设施。
## `mut`、未使用变量等(https://blog.dureuill.net/articles/wip/#mut-unused-variables-etc)
编译器提供的默认警告在开发过程中非常有用。它们会捕捉到:
- 未使用的绑定
- 不必要地可变的绑定
- 未使用的结果和选项
所有这些问题往往出现在代码尚未完成开发时。例如,一个完全由 `todo!()` 组成的函数,通常会对其参数产生这样的警告。
⚠️ 重要的是,要抵制**在开发过程中临时修复这些警告**的冲动,例如通过给绑定加 `_` 前缀或其他技巧。如果你人为地修复了警告,就有可能将未完成的代码交出去。忽略警告有其场合,但这里不是!
## “TODO”文档字符串
在编码时,我经常想到有些东西需要修复或处理,比如一个边界情况或一些不完整/有问题的逻辑。与显式错误处理不同,这类问题不在类型系统中跟踪,所以我通常会留下一段注释,提醒自己修复问题,如果我不想被当前正在实现的逻辑分心的话。
这也挺好,但我需要记得事后真的回去处理 😅
为了做到这一点,我经常使用一个技巧:将这些注释用三个斜杠 `///` 开头,就像文档注释那样(https://doc.rust-lang.org/reference/comments.html#r-comments.doc.syntax)。
Rust 会警告不描述任何东西的文档注释,所以把文档注释放在一条语句上面,通常就能达到生成警告的目的,这个警告与注释中描述的问题相关联。
## 我的警告在哪里?原生 Rust 的局限(https://blog.dureuill.net/articles/wip/#where-are-my-warnings-limitations-of-vanilla-rust)
这些技巧我用了很多年(我写 Rust 已经超过十年了...),但我对它们还是有些不满。
总体而言,最令人头疼的问题是,并非所有这些技巧都能可靠地为需要解决的问题生成警告,而这些问题需要在代码投入生产之前解决。
忘记移除 `unwrap` 并在发布后看到它爆炸,是工程师最糟糕的感受之一 😓
`todo!` 是这类问题中一个常见而又奇怪的案例。如前所述,它经常会产生警告,但这些警告是间接的,例如由函数参数未使用或可变操作未发生时引起的。因此,它会变得有点危险,因为人们往往习惯于在使用 `todo!` 时经常看到警告,并依赖它们的缺失来点击合并按钮。
`unwrap` 和 `clone` 更糟糕,它们从不发出警告,所以你必须格外小心地重新阅读,以在最终 PR 前移除它们。
它们的另一个问题是,其中一些**本就是意在生产环境中使用的**(https://burntsushi.net/unwrap/)。按照本文中概述的“随意使用 unwrap”策略,我们将 `unwrap` 的正常用例与其“WIP”用例混为一谈了。
最后,“TODO 文档字符串”有点 hacky,也许可以说它是一个快乐的意外?在这种能力下,它有着不直观的失败模式,因为目前不允许在表达式后使用文档注释,否则会导致编译错误。这种情况不常发生,但我们也面临在实际上会生成文档的地方使用 `///` 的风险,这不太适合发布,而且不会产生预期的警告。
## 介绍 `wip` crate(https://blog.dureuill.net/articles/wip/#introducing-the-wip-crate)
为了回应对编写 WIP Rust 时所有这些小痛点的反应,我决定开发一个名为 `wip` 的 crate(https://codeberg.org/dureuill/wip/),以便更容易实现“稍后警告”策略。
它主要包含一组工具,你可以在开发 Rust 的任何上下文中使用。
## 使用 `wip`(https://blog.dureuill.net/articles/wip/#using-wip)
像添加其他 crate 一样,将 `wip` 添加到你的依赖中:
```
cargo add wip
```
可选地,你可以在需要使用 `wip` 的文件中导入它提供的 prelude:
```
use wip::prelude::*;
```
由于 `wip` 依赖于 Rust 的废弃警告,我建议你禁用编辑器中删除已弃用函数调用的功能,否则会变得视觉上很突兀。
## `wip::wip!`,一个会发出警告的 `todo!`(https://blog.dureuill.net/articles/wip/#wip-wip-a-warning-emitting-todo)
`wip` 提供的第一个工具是 `wip!` 宏,它的工作方式与 `todo!` 完全相同,但使用时总会发出警告。在你使用 `todo!` 的任何地方使用 `wip!`,将代码标记为未完成。
### 为什么不用 `todo!`?(https://blog.dureuill.net/articles/wip/#why-not-todo)
`wip` 的早期版本使用了 `todo` 这个名称,但这会阻止我们使用 prelude,因为 Rust 不喜欢依赖试图遮蔽 `std` 宏...
## `unwrap_wip` 和 `clone_wip`(https://blog.dureuill.net/articles/wip/#unwrap-wip-and-clone-wip)
这些方法作为 `Result` 和 `Option` 的扩展 trait 来实现。当在作用域中时,它们的工作方式与普通的 `unwrap` 和 `clone` 完全相同,只是在使用时发出警告。
它们还明确表示你不打算让这个 unwrap/clone 留在最终代码中!不过,请记住,事情并不总是那样发展...
"毕竟,为什么不呢" meme
## `wip::fixme!`,一个不 panic 的备忘宏(https://blog.dureuill.net/articles/wip/#wip-fixme-a-non-panicking-macro-for-memos)
我从 `wip` 中使用最多的工具之一是 `fixme!` 宏。它是 `wip!` 的一个不 panic 的变体,不代表一个表达式。它的工作方式与 TODO 注释相同,但没有了 hacky 的部分,并且带有可靠的警告。
## 拥抱我们的 WIP 未来(https://blog.dureuill.net/articles/wip/#embracing-our-wip-future)
我已经在我最近的一些 PR 中使用了 `wip` crate,包括一些我特别大的 PR(https://github.com/meilisearch/meilisearch/pull/6484)。通常我会在开发期间将 `wip` 依赖添加到工作空间的 crate 中,然后在审查前重写历史时移除它。
这是一种解放的体验,完全属于“我没想到我会这么需要它”这一类。它让我更容易专注于快乐路径,同时不会忘记以后要做的事情。
Buzz Lightyear “到处 X” meme
还有什么不喜欢的呢?
我能想到的唯一缺点:
1. 添加 `fixme` 时需要写足够的上下文,否则后来回来修复时可能会变得困难。这通常不是问题,因为 `wip` 通常只出现在(通常)较短的单作者(或结对编程)的 PR 中。
2. 有时 `wip` 导致的警告数量会变得有点令人沮丧,尤其是在较大的 PR 中。或许避免那些?😅
尽管如此,我很确定我会继续使用 `wip`。你在开发过程中是如何使用警告的?你觉得当前的 API(https://docs.rs/wip/latest/wip/)可以增加哪些有用的功能?
相似文章
Rust错误处理的新视角
本文讨论了Rust中不同的错误处理模式,包括panic、使用Option和Result、以及默认恢复机制,并提出了一种超越简单传播或恢复的错误处理方法。
自定义错误在我的Rust应用中是必不可少的
一篇提倡在Rust应用中使用自定义错误类型的博客文章,解释了如何使用map_err和From trait创建一个统一的AppError枚举,以简化不同子系统间的错误处理。
从Go迁移到Rust
一份为Go开发者迁移到Rust编写的全面指南,专注于后端服务,对比正确性、运行时和人体工程学方面的权衡,并提供关于渐进式迁移的实用建议。
Rust中的作用域错误
Kan-Ru Chen 介绍了 `scoped-error`,一个新的 Rust crate,旨在通过将上下文附件限定在模块级别来改善错误处理的人机工学,解决了像 anyhow 和 thiserror 等现有 crate 的问题。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。