☰
100-exercises-to-learn-rust 第 07 关:`Result` 的展开艺术——`unwrap`、`expect` 与 `match` 三选一
2026/10/3 2:07:13 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】100-exercises-to-learn-rust

A self-paced course to learn Rust, one exercise at a time.

项目地址:https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust
点击查看免费下载

导读

在上一节06_fallibility中,Ticket::new不再 panic,而是返回Result<Ticket, String>,把错误的处理权交还给了调用方。但"把错误交还回来"之后,调用方到底该怎么接住它?这正是本节07_unwrap要解决的问题。读完本文,你将掌握 Rust 中消费Result的三种核心方式——unwrap、expect与match,并能结合 07_unwrap 练习 中easy_ticket的实战场景,理解"何时该 panic、何时该优雅降级"的取舍。

背景:Ticket::new变成了Result

在 06_fallibility 中,Ticket::new的签名从返回Ticket变成了返回Result<Ticket, String>:一旦校验失败,不再panic!,而是构造并返回一个Err。这一改动彻底改变了调用方的处境——调用方再也不能假装失败不会发生。

从练习源码 07_unwrap/src/lib.rs 可以看到Ticket::new的真实校验逻辑:

impl Ticket { pub fn new(title: String, description: String, status: Status) -> Result<Ticket, String> { if title.is_empty() { return Err("Title cannot be empty".to_string()); } if title.len() > 50 { return Err("Title cannot be longer than 50 bytes".to_string()); } if description.is_empty() { return Err("Description cannot be empty".to_string()); } if description.len() > 500 { return Err("Description cannot be longer than 500 bytes".to_string()); } Ok(Ticket { title, description, status }) } }

现在问题来了:拿到这个Result之后,调用方下一步该做什么?

失败无法被(隐式地)忽略

这是 Rust 错误处理与异常机制最本质的区别:不同于异常,Result强制你在调用点处理错误。

如果你调用一个返回Result的函数,Rust 编译器不会允许你隐式地忽略错误分支。原文档给出了一个反例:

fn parse_int(s: &str) -> Result<i32, ParseIntError> { // ... } // 这段代码无法编译:我们没有处理错误分支。 // 我们必须使用 `match` 或 `Result` 提供的某个 combinator // 来"展开"成功值,或处理错误。 let number = parse_int("42") + 2;

为什么let number = parse_int("42") + 2;编译不过?因为parse_int返回的不是i32而是Result<i32, ParseIntError>,对它做+ 2运算在类型上就是非法的。编译器会直接报错,迫使你面对这样一个事实:这个函数可能会失败,你还没有想好失败时该怎么办。

对比一下 06_fallibility 中关于异常机制的讨论:在 Python、C# 等语言里,仅看函数签名你根本不知道它会不会抛异常、会抛哪种异常,抛异常的位置与捕获异常的位置之间隔着千山万水,逻辑局部性很差。而 Rust 把"可能失败"这一信息编码进了类型系统——只要签名里出现Result,调用方就一目了然。

这也呼应了本教程的核心方法论:fallibility(易错性)必须是显式的。panic!依然存在,但它面向的是不可恢复的错误,应当谨慎使用。

你拿到了一个Result。现在怎么办?

原文档给出两种关键选择:panic或显式解构。

选择一:失败即 panic——unwrap与expect

如果你确信操作不会失败,或者失败本身就是一个无法恢复的程序错误,可以调用Result的两个方法:

// 如果 `parse_int` 返回 `Err`,这里会 panic。 let number = parse_int("42").unwrap(); // `expect` 允许你指定自定义的 panic 消息。 let number = parse_int("42").expect("Failed to parse integer");

两者行为上的区别只有一个:panic 时携带的错误信息。unwrap()的 panic 消息是Err值自身的Debug输出;expect(msg)则会把msg作为 panic 消息前缀,便于后续排查。实践中更推荐expect——当几个月后 panic 真的发生时,一句"Failed to parse integer"远比一堆裸数据更能帮你定位问题。

注意:unwrap和expect会把Ok(T)里的T直接解出来,同时丢弃Err(E)分支的所有处理余地。它们本质上是把Result变回"要么成功、要么崩溃"的二元世界,所以只应在"失败等同于程序 bug"的场合使用。

选择二:用match显式处理错误分支

如果你希望针对失败做出有意义的响应(打印日志、使用默认值、返回另一条错误等),就应当用match把Result解构开来:

match parse_int("42") { Ok(number) => println!("Parsed number: {}", number), Err(err) => eprintln!("Error: {}", err), }

match是穷尽的:Result只有Ok和Err两个变体,编译器会要求你两个分支都覆盖到。这种穷尽性正是 Rust 错误处理可靠性的来源——你永远不会"忘了写else"。

关于match的展开细节,可以回顾本教程前面的章节:在 03_variants_with_data 中你已经学会了用模式匹配解构带数据的变体(如Status::InProgress { assigned_to }),而 04_if_let 则展示了只关心单个变体时用if let/let-else收窄分支的写法。Result的Ok/Err分支同样可以配合这些模式匹配语法使用。

实战演练:easy_ticket中的三种处理方式

理解了unwrap/expect/match的分工后,让我们看看它们如何在 07_unwrap 练习 中组合使用。练习要求实现easy_ticket:

// `easy_ticket` 在标题非法时应 panic; // 而当描述非法时,则改用默认描述:"Description not provided"。 fn easy_ticket(title: String, description: String, status: Status) -> Ticket { todo!() }

这是一个非常典型的"混合策略"场景,恰好把本文的两种选择都用上了:

  1. 标题校验失败 → panic。标题是票据的核心标识,为空或超长意味着调用方传入了根本无效的数据,这属于不可恢复的程序错误,直接用expect展开即可。练习的测试用#[should_panic(expected = "Title cannot be empty")]与#[should_panic(expected = "Title cannot be longer than 50 bytes")]明确验证了这一点(见 lib.rs 测试模块)。注意这里的expected字符串与Ticket::new返回的Err消息完全对应——这正是原文档 08_error_enums 所警示的"字符串匹配"脆弱性:一旦错误消息被同事改写,测试和调用代码都会跟着断掉。这也预告了后续章节会引入错误枚举来消除这种耦合。

  2. 描述校验失败 → 优雅降级。描述是可选的补充信息,缺失时不应该让程序崩溃,而是用默认文案兜底。这里就需要对Err分支做出反应,而不是简单 panic。

一个符合要求的实现大致长这样(伪代码示意):

fn easy_ticket(title: String, description: String, status: Status) -> Ticket { match Ticket::new(title, description, status) { Ok(ticket) => ticket, Err(e) if e.contains("Title") => panic!("{}", e), // 标题错误:panic Err(_) => Ticket::new( "fallback title".to_string(), "Description not provided".to_string(), status, ).expect("fallback should always be valid"), // 描述错误:用默认描述重建 } }

更简洁的写法是:先用Ticket::new对标题做一次expect,再对含描述的分支用match或unwrap_or_else兜底。无论哪种写法,核心思路一致——把Result的两种变体分别路由到"panic"和"恢复"两条路径上,这正对应原文档给出的两个选项。

练习中template_description_is_used_if_empty与template_description_is_used_if_too_long两个测试(见 lib.rs 测试模块)验证了降级行为:传入空描述或超长描述时,得到的票据描述都应是"Description not provided"。测试数据(如overly_long_description、valid_title)来自 helpers/common 测试辅助 crate,这也体现了本教程用独立 helper crate 复用测试数据的组织方式。

如何运行与验证

该练习是一个独立的 Rust crate,位于 exercises/05_ticket_v2/07_unwrap/。其 Cargo.toml 声明了edition = "2021",并仅在 dev-dependencies 中引入commonhelper crate。在仓库根目录下进入该目录后执行:

cargo test

即可看到上述 4 个测试全部通过:两个#[should_panic]测试验证标题错误触发 panic(且 panic 消息精确匹配),两个普通测试验证描述非法时自动替换为默认文案。动手把easy_ticket的todo!()补全,再跑一遍测试,你就能直观感受到unwrap/expect/match三者"各司其职"的完整闭环。

小结

  • Result是值不是异常:失败被编码进函数签名,调用点必须显式处理,否则无法编译。
  • unwrap/expect:把失败转化为 panic,适合"失败即程序 bug"的场景;需要自定义 panic 信息时优先expect。
  • match(以及if let/let-else):对Ok/Err分支分别做出响应,适合需要恢复、降级或传播错误的场景。
  • 实战取舍:easy_ticket示范了同一次调用中"标题失败就 panic、描述失败就降级"的混合策略——选择哪种展开方式,取决于失败本身是否可恢复。

下一步,08_error_enums 将解决"用字符串当错误类型"的脆弱性,引入错误枚举把错误案例编码进类型系统——那正是unwrap与match得以优雅共存的关键基础设施。

  • 示例工程
  • 教程

【免费下载链接】100-exercises-to-learn-rust

A self-paced course to learn Rust, one exercise at a time.

项目地址:https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust
点击查看免费下载
上一篇:零基础掌握喜马拉雅音频下载工具:从安装到精通的完整指南
下一篇:如何用Starless生成逼真黑洞图像?完整安装与快速入门指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询