Work In Progress Rust
Work In Progress Rust
Rust is a reliable language that prioritizes correctness. However, during development, it can be a burden to account for every single error path immediately. This article presents a few techniques that defer handling correctness to that you can stay longer in the happy path, as well as a library I developed to make this process more convenient. Rust 是一门优先考虑正确性的可靠语言。然而,在开发过程中,立即处理每一个错误路径可能会成为一种负担。本文介绍了几种推迟处理正确性的技巧,让你能更长时间地停留在“快乐路径”(happy path)上,同时也介绍了我开发的一个让这一过程更方便的库。
🌱 100% of this article has been handcrafted without using generative AI tooling, if somewhat hastily. Take my typoes and broken sentences as proof of that. 🌱 本文 100% 由人工撰写,未使用任何生成式 AI 工具,尽管写得有些仓促。文中的错别字和病句就是最好的证明。
What’s worse for a Rust developer working on some elaborate logic than having rustc suddenly getting in the way? Be it because you didn’t return a Result yet, or handle this error case, or implement that edge case, causes for compilation errors are numerous and can distract you from what you’re attempting to achieve.
对于正在编写复杂逻辑的 Rust 开发者来说,还有什么比 rustc 突然跳出来阻碍你更糟糕的呢?无论是还没返回 Result,还是没处理某个错误情况,亦或是没实现某个边界条件,编译错误的原因不胜枚举,它们会让你偏离原本想要实现的目标。
When this happens you’re usually presented with two options: 当这种情况发生时,你通常有两个选择:
-
Go with the distraction and immediately implement perfect error handling. Typically you’ll have to refactor it a number of times before the PR is ready, making the whole process much more heavy than what is necessary.
-
顺从干扰,立即实现完美的错误处理。通常在 PR 准备好之前,你必须进行多次重构,这使得整个过程比实际需要的沉重得多。
-
Slap some manner of TODO later label on the error, downgrading it to a warning, and move on with your work for now. This article covers some Rust techniques to achieve the latter option, downgrading an immediate error to a warning to be handled later.
-
给错误贴上某种“稍后处理”的 TODO 标签,将其降级为警告,然后继续手头的工作。本文将介绍一些实现后者的 Rust 技巧,即将即时错误降级为稍后处理的警告。
Why do you want warnings? Warnings occupy the perfect spot in the possible gamut of compilation interactions: Errors block other compilation steps (lifetimes, etc), and testing, impeding the common workflow: 为什么要使用警告?警告在编译交互的各种可能性中占据了绝佳的位置:错误会阻塞其他编译步骤(如生命周期检查等)和测试,从而阻碍了常规的工作流:
- Write WIP code
- Test it
- Turn WIP code to production code
- 编写 WIP(进行中)代码
- 测试代码
- 将 WIP 代码转化为生产代码
Any error prevents going from (1) to (2). Completely suppressing any compilation message, on the other hand, runs the risk of forgetting about the issues and letting them sweep into production 😬. Warnings don’t block development, but need to be cleaned before merging the PR. At Meilisearch, we have a CI that runs the compiler with the -D warning flag, that will turn all warnings into errors at CI time, ensuring we don’t ship WIP code accidentally.
任何错误都会阻止从 (1) 到 (2) 的过程。另一方面,完全抑制编译信息则有忘记问题并将其带入生产环境的风险 😬。警告不会阻塞开发,但在合并 PR 之前必须清理干净。在 Meilisearch,我们的 CI 在运行编译器时会加上 -D warning 标志,这会在 CI 阶段将所有警告转化为错误,确保我们不会意外发布 WIP 代码。
Why don’t you just read the f**king code??? Of course, I do. Reading code (yours, and the one from your colleagues) is an excellent barrier against bugs, and code reviews are a requirement to effectively share knowledge among your team. All the techniques discussed in the article come as an additional manner of creating incentives for correctness and stronger guardrails when producing code at a professional scale. They increase quality but don’t replace the basic good practices. In my experience as a software engineer, anything that relies on humans never a mistake is deemed to fail eventually. 为什么不直接去读代码呢???当然,我会读。阅读代码(你自己的以及同事的代码)是防止 Bug 的绝佳屏障,代码审查也是团队间有效共享知识的必要条件。本文讨论的所有技巧都是在专业规模下编写代码时,创造正确性激励和更强防护栏的额外手段。它们能提高质量,但不能取代基本的良好实践。以我作为软件工程师的经验来看,任何依赖于“人类永不犯错”的事物最终都会失败。
Without further ado, here are some techniques you can use in vanilla Rust to implement error deferral strategies. 废话少说,以下是一些你可以在原生 Rust 中使用,以实现错误推迟策略的技巧。
Techniques with standard Rust
原生 Rust 技巧
unwrap everywhere 到处使用 unwrap
While it is important that Rust gives us the structure to correctly address errors, it doesn’t mean we need to address them immediately. A common pattern for delaying the error handling is to simply unwrap your Result or Option, to get the value inside at the price of a panic on the error path. Then when the rest of the code is ready, you can go back to your changes and selectively remove the unwrap that were added as a temporary measure, replacing them with proper error handling.
虽然 Rust 为我们提供了正确处理错误的结构很重要,但这并不意味着我们需要立即处理它们。推迟错误处理的一个常见模式是简单地对 Result 或 Option 使用 unwrap,以获取内部的值,代价是在错误路径上触发 panic。当其余代码准备好后,你可以回头修改,有选择地移除作为临时措施添加的 unwrap,并用适当的错误处理替换它们。
fn does_not_return_result_yet(some_params: SomeType) -> SomeReturnType {
let converted = some_params.try_into().unwrap() // <- fallible conversion, ignore for now
converted.try_foo().unwrap() // <- fallible operation, ignore for now
}
clone everywhere 到处使用 clone
Ownership is a similar issue, albeit slightly different and much less common than error handling. Properly maintaining ownership sometimes mandates design modifications, lest lifetime issues appear. In this case, a frequent mitigation option is to clone a value instead of moving it around. Delaying the fixes for this category of issues is a bit more delicate, as the fix can entail changing the architecture of the code in non-trivial ways. Still, with experience, it is possible to identify early cases where you’ll be able to remove the clones later by passing the appropriate variables higher in the stack. 所有权是一个类似的问题,尽管它与错误处理略有不同且不太常见。正确维护所有权有时需要修改设计,否则会出现生命周期问题。在这种情况下,一种常见的缓解方案是克隆一个值而不是移动它。推迟这类问题的修复比较棘手,因为修复可能涉及以非平凡的方式更改代码架构。不过,凭借经验,你可以尽早识别出哪些情况可以在稍后通过将适当的变量传递到堆栈更高层来移除克隆。
the todo! macro todo! 宏
The Rust standard library provides a todo!() macro, whose purpose is described as: Indicates unfinished code. This can be useful if you are prototyping and just want a placeholder to let your code pass type analysis. This is indeed extremely useful when shaping out an interface and needing to make some function definitions available, without actually having to provide a working implementation. Because todo! diverges (it panics), the compiler is able to use it as a placeholder expression for most types, hence the reference to type analysis in the docs. A liberal use of todo allows deferring some code paths (and not just errors) to be implemented at a later time.
Rust 标准库提供了一个 todo!() 宏,其用途描述为:指示未完成的代码。如果你正在进行原型设计,并且只需要一个占位符让代码通过类型分析,这会很有用。在构建接口并需要提供某些函数定义,而无需实际提供可运行的实现时,这确实非常有用。因为 todo! 会发散(它会 panic),编译器能够将其用作大多数类型的占位符表达式,这就是文档中提到类型分析的原因。自由使用 todo 可以推迟某些代码路径(不仅仅是错误)的实现。
Digression: not to be confused with unimplemented!() 题外话:不要与 unimplemented!() 混淆
I saw at least once someone being wrong on the Internet about todo!(): they would state that it was meant to communicate to users of a program that some features were not yet implemented. Rust actually has a dedicated macro for this use case, and its name is unimplemented!(). todo!() has a different audience: the author of the code and their reviewers. No todos are meant to hit production, they are a WIP Rust amenity only.
我至少不止一次在网上看到有人对 todo!() 的理解是错误的:他们声称它的目的是向程序用户传达某些功能尚未实现。Rust 实际上有一个专门用于此用例的宏,它的名字是 unimplemented!()。todo!() 的受众不同:它是给代码作者及其审查者看的。todo 不应该出现在生产环境中,它们只是 WIP Rust 的便利设施。
mut, unused variables, etc. mut、未使用的变量等
The default warnings provided by the compiler are very useful while developing. They will catch: 编译器提供的默认警告在开发过程中非常有用。它们会捕获:
- unused bindings (未使用的绑定)
- needlessly mutable bindings (不必要的 mut 绑定)
- unused results and options (未使用的 Result 和 Option)
All of these tend to appear when code isn’t finished developing. For instance, a function whose implementation consists solely of todo!() will typically have such warnings about their parameters.
所有这些往往在代码尚未开发完成时出现。例如,一个实现仅包含 todo!() 的函数通常会对其参数产生此类警告。
⚠️ It is important to resist the urge to temporarily fix these warnings during development, by prefixing the bindings with _ or other tricks. You risk handing over the unfinished code if you artifically fix the warnings. Ignoring warnings has its place, but it…
⚠️ 在开发过程中,抵制通过在绑定前加 _ 或其他技巧来临时修复这些警告的冲动非常重要。如果你人为地修复了警告,就有可能提交未完成的代码。忽略警告有其适用场景,但它……