Rust错误处理的新视角

Lobsters Hottest 工具

摘要

本文讨论了Rust中不同的错误处理模式,包括panic、使用Option和Result、以及默认恢复机制,并提出了一种超越简单传播或恢复的错误处理方法。

<p><a href="https://lobste.rs/s/l0yqco/novel_look_at_error_handling_rust">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/06 02:00

# 对 Rust 错误处理的新颖看法 来源:https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html 关于 Rust 中的错误处理,人们已经说了很多。有大量不同的观点,涉及如何组织错误、何时使用 panic,以及 proc-macros 在错误处理中的作用。大多数关于 Rust 错误处理的讨论都假设:当错误发生时,要么将错误向上传递给调用者,要么尝试完全恢复并继续执行。虽然这描述了许多情况,但并未涵盖所有情况。我想从不同角度看待错误处理以及如何处理它。这并不是要取代主流错误传播的混乱做法,而是希望在您的工具箱中增加一个新工具。 ## 现状概述 如果某个函数调用了另一个返回 `Result` 的函数,那么在 Rust 中有几种不同的处理方式。为了说明这一点,假设我们有一个函数,它接受两个字符串,将它们解析为 `u32` 并相加。以下是几种处理解析失败的方式。 它可能直接 panic。在这种情况下,通常会在函数的文档中解释这一点。 ```rust /// 将两个字符串解析为 `u32` 并相加 /// /// # Panics /// /// 如果提供的字符串无法解析,则 panic fn add_strs_panic(s1: &str, s2: &str) -> u32 { let u1 = s1.parse().expect("s1 应该是一个有效的 `u32`"); let u2 = s2.parse().expect("s2 应该是一个有效的 `u32`"); u1 + u2 } ``` 它可能返回 `Option`。这种方法通常会利用标准库提供的组合子,如 `Option::ok`,与问号操作符结合使用。 ```rust /// 将两个字符串解析为 `u32` 并相加结果 /// /// 如果解析失败,则返回 `None` fn add_strs_option(s1: &str, s2: &str) -> Option<u32> { let u1 = s1.parse().ok()?; let u2 = s2.parse().ok()?; Some(u1 + u2) } ``` 它可能返回一个结果类型。这通常涉及以某种方式包装错误。 ```rust enum AddStrsError { // 以优雅方式包装错误的 variant(为了简洁省略) } /// 将两个字符串解析为 `u32` 并相加结果 /// /// 如果解析失败则返回错误 fn add_strs_result(s1: &str, s2: &str) -> Result<u32, AddStrsError> { let u1 = s1.parse().map_err(/* 为错误提供更多上下文 */)?; let u2 = s2.parse().map_err(/* 为错误提供更多上下文 */)?; Ok(u1 + u2) } ``` 甚至还有一种不错的方法来处理完全不失败的情况,而是从失败中恢复。函数可以将错误的输入当作 `0` 或其他随机值处理。(顺便提一句,我认为这种轻松而又干净地从错误中恢复的能力,是“错误即值”的主要好处之一。) ```rust /// 将两个字符串解析为 `u32` 并相加结果 /// /// 如果解析失败,则将值默认为 0 fn add_strs_default(s1: &str, s2: &str) -> u32 { // 我们可以使用 `unwrap_or_default`,但我觉得 0 更清晰 let u1 = s1.parse().unwrap_or(0); let u2 = s2.parse().unwrap_or(0); u1 + u2 } ``` ## 问题所在 最后一个例子有些牵强,对于那个例子可能不太合理,但我确实写过类似这样的代码。“默认值”代码存在一个问题:函数的调用者无法知道是否进行了默认处理。函数从错误中恢复并继续执行是完全合理的。同样,调用者想要知道函数内部发生了某些失败也是合理的。问题在于,当这两个情况同时发生时,缺少一种解决方案。 这种同时发生失败和继续执行的现象比较常见。以下几个(或许更现实的)例子包括: - 收集编译器在特定阶段发生的所有错误。 - 如果文件路径不存在,则使用默认文件路径。 - 将除法运算符分母为零的情况视为求值结果为 0¹ - 即使某些测试失败了,仍然运行整个测试套件中的所有测试² ## 解决方案 1:Go 方式 很有意思的是,Go 编程语言处理错误的方式并没有这个问题。对于那些不熟悉的人,我会做一个极简的概述。 `Go` 和 Rust 一样,采用“错误即值”。在 Go 中,错误是实现 `Error` 接口的任何类型。与 Rust 不同,`Go` 没有真正的和类型。因此,可以返回一个元组,包含正常路径的值和一个 `error` 类型,如下所示: ```go func AddStrsDefault(s1 string, s2 string) (uint64, error) { var err error i1, err := strconv.ParseUint(s1, 10, 32) // 注意:如果两个函数都返回错误,只有第二个错误会被看到 i2, err := strconv.ParseUint(s2, 10, 32) return i1 + i2, err } ``` 由于在 `Go` 中任何指针都可以是 `nil`,如果没有错误,返回的值就设为 `nil`。当我们在 Rust 中遇到上述问题时,这可能是一种可行的方案。如果我们的计算总是会完成,但也可能存在错误,我们可以返回一个可选的错误: ```rust fn add_strs_default(s1: &str, s2: &str) -> (u32, Option<Error>) { let mut ret_err: Option<Error> = None; // 我们本可以用 `unwrap_or_default`,但我觉得 0 更清晰 let u1 = match s1.parse() { Ok(val) => val, Err(err) => { ret_err = Some(/* 映射错误 */); 0 } }; let u2 = match s2.parse() { Ok(val) => val, Err(err) => { ret_err = Some(/* 映射错误 */); 0 } }; (u1 + u2, ret_err) } ``` 然后函数的调用者可以选择传播错误,或者直接使用提供的值。需要注意几点: - 要支持返回多个错误,修改起来很容易。 - 这不如 Go 代码或上述任何变体那样符合人体工程学。主要是因为代码大部分被错误处理代码所消耗。如果这个函数实际上很复杂,我可以想象使用这种模式的函数会变得难以阅读。 - 类型签名并没有真正表明函数可能失败。 我认为对于相对简单的单次函数,这个解决方案是可行的,但如果你的代码中有很多类似函数,或者代码逻辑过于复杂,我认为它解决不了问题。 ## 我们真正想要的 为了提出解决方案,我认为有必要列出我们想要的目标。这也有助于判断什么时候需要多个解决方案。针对这个问题,我想要以下几点: - 应该能够以某种默认值继续工作 - “正常路径”应该尽可能主导逻辑 - 函数的签名应该清楚表明失败的可能性 - 应该容易返回多个错误 - 应该能够与“传统 Rust 错误处理”组合 - 应该能够与自身组合 - 仍然应该能够从函数中提前返回 - 应该尽可能“地道” ## 解决方案 2:错误参数 我提出的第一个解决方案称为“错误参数”。其思想是向可失败函数传递一个参数,这个参数能够以多种不同方式收集错误。这对于那些想要从应用程序的大部分区域收集错误的场景尤其有用,因为这些错误通常是整体性的。创建这样一个东西几乎不需要什么努力,但我认为在生态系统中,像 `anyhow` 这样的 crate 可以作为更全面的 API 来帮助实现这种方法。³ 以下是一个简单版本的样子: ```rust // 你也可以创建一个只保存特定错误的变体。 #[derive(Default)] struct MultiErrors(Vec<Box<dyn Error>>); impl MultiErrors { /// 处理一个 `Result`,将错误内部添加进去 /// /// 这与标准库中的 `Result::ok` 函数非常相似。 // 一个更符合人体工程学的版本可能会以某种形式使用内部可变性 fn handle_result<T, E: Error>(&mut self, res: Result<T, E>) -> Option<T> { match res { Ok(val) => Some(val), Err(err) => { self.0.push(Box::new(err) as Box<dyn Error>); None } } } /// 检查内部是否有任何错误 fn is_ok(&self) -> bool { self.0.is_empty() } /// 获取所有遇到的错误 fn get_errs(&self) -> &[Box<dyn Error>] { &self.0 } /// 转换为一个 `Result` fn as_result(&self) -> Result<(), &[Box<dyn Error>]> { if self.is_ok() { Ok(()) } else { Err(self.get_errs()) } } } ``` 你可以像这样使用这个类型: ```rust fn add_strs_result( s1: &str, s2: &str, errs: &mut MultiErrors ) -> u32 { let u1 = errs.handle_result(s1.parse()).unwrap_or(0); let u2 = errs.handle_result(s2.parse()).unwrap_or(0); u1 + u2 } ``` 我认为这是一个相当不错的方法。它满足了最初列出的大部分需求。它既能与自身组合,也能与“传统 Rust 错误处理”组合。大大减少了处理单个错误所需的代码量,并且支持多种不同的使用场景。我认为这是该问题的一个非常好的解决方案。 尽管如此,我并不认为这种方法完美无缺。这种方法不强制你处理(或至少承认)失败,它不是世界上最“地道”的方法,返回类型也没有传达该函数可能失败的信息(尽管函数类型整体上传达了)。可能还有其他解决方案,在不同的权衡中做出选择。⁴ 我特别想看看如何利用不稳定的 `Try` trait 来解决这个问题。 ## 结论 Rust 中传统的错误处理非常不错。但对于某些类型的任务来说,它还不够充分。具体来说,它不适用于你想继续工作同时又将错误报告给调用者的场景。Go 处理错误的方式可行,但不能与传统错误处理组合。我认为更好的替代方案是将一个 `MultiErrors` 结构体传递给调用者,由它来跟踪函数运行过程中遇到的错误。 希望这能揭示 Rust 错误处理中一个有趣的问题,并引发更多关于如何在 Rust 中处理错误的讨论。 --- ¹ https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:1 ² https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:2 ³ https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:3 ⁴ https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:4

相似文章

Rust 中的进行中工作

Lobsters Hottest

本文介绍了一种在 Rust 开发过程中延迟错误处理的技术和库,允许开发者临时将错误降级为警告,从而在保持正确性的同时维持生产力。

Rust中的作用域错误

Lobsters Hottest

Kan-Ru Chen 介绍了 `scoped-error`,一个新的 Rust crate,旨在通过将上下文附件限定在模块级别来改善错误处理的人机工学,解决了像 anyhow 和 thiserror 等现有 crate 的问题。

Rust - 在 map 闭包中处理 Result

Lobsters Hottest

本文探讨了在 map 闭包中使用返回 Result 的函数时常见的 Rust 错误,并提供了多种解决方案:收集结果、使用循环、filter_map 和 try_fold。

Rust中Pretty Printer实现的新设计

Lobsters Hottest

一篇博客文章,探讨了Rust中Pretty Printer实现的新设计,解决了将函数式编程研究适应到没有垃圾回收的系统语言中的挑战,并比较了现有的方法,比如'pretty' crate和Oppen风格的Pretty Printer。