Comprehensive Rust 错误处理实战:用 `Result` 与 `?` 重写表达式求值器

发布时间:2026/9/10 12:53:01
Comprehensive Rust 错误处理实战:用 `Result` 与 `?` 重写表达式求值器 Comprehensive Rust 错误处理实战用Result与?重写表达式求值器【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust本篇文章聚焦 Google Android 团队维护的 Rust 教学课程 Comprehensive Rust仓库根目录comprehensive-rust中“错误处理”章节的课堂练习及其标准答案。文章以 solution.md 为骨架完整呈现如何把原本遇到除零时直接panic!的表达式求值器eval改写成返回Resulti64, DivideByZeroError的惯用错误处理版本并深入解析Result枚举、?运算符、Ok包装、单元结构体错误类型等核心机制。读完你将掌握如何通过类型签名让“可能失败”成为函数契约的一部分、如何用?让错误传播简洁清晰、以及如何用单元测试验证错误路径。练习背景从 Day 2 求值器出发本练习见 exercise.md是对课程第 2 天“表达式求值器”练习的 revisit。原实现忽略了一个关键错误场景除以零。练习要求把eval重写为使用惯用错误处理方式在除零发生时返回错误而不是崩溃。练习文档特意指出一个容易让学员困惑的细节起始代码与之前练习的答案并不完全相同——仓库在原始代码中显式加入了panic!(Cannot divide by zero!)目的是向学员指明错误用例所在的位置见 exercise.md 的details提示块。如果你发现代码和记忆中的“正确答案”有出入这正是刻意为之。题目同时提供了一个预置的错误类型DivideByZeroError作为eval的错误类型学员无需自己设计错误类型可以把全部精力放在Result改造上。起始代码暴露问题的“旧世界”练习的起始代码位于 exercise.rs 的eval锚点段// ANCHOR: eval// The original implementation of the expression evaluator. Update this to // return a Result and produce an error when dividing by 0. fn eval(e: Expression) - i64 { match e { Expression::Op { op, left, right } { let left eval(*left); let right eval(*right); match op { Operation::Add left right, Operation::Sub left - right, Operation::Mul left * right, Operation::Div if right ! 0 { left / right } else { panic!(Cannot divide by zero!); }, } } Expression::Value(v) v, } }这段代码有几个典型的“非惯用”问题错误路径不可见函数的返回类型是i64调用者从类型签名上完全看不出它会“爆炸”只能靠文档或运行时崩溃发现风险。用 panic 表达可预期错误除零是表达式求值过程中完全可预见的输入错误属于“可恢复错误”范畴却用panic!处理。课程在 panics.md 中明确panic 只用于不可恢复的、意外发生的错误是程序 bug 的症状断言失败、越界访问会 panic而“除数为 0”这种业务输入错误应该走Result通道。支撑类型Operation与Expression练习与答案共用同一组类型定义exercise.rs 的// ANCHOR: types段它们决定了求值器的数据结构/// An operation to perform on two subexpressions. #[derive(Debug)] enum Operation { Add, Sub, Mul, Div, } /// An expression, in tree form. #[derive(Debug)] enum Expression { /// An operation on two subexpressions. Op { op: Operation, left: BoxExpression, right: BoxExpression }, /// A literal value Value(i64), } #[derive(PartialEq, Eq, Debug)] struct DivideByZeroError;三个关键点值得展开Expression是递归的树形结构Op变体通过BoxExpression持有左右两个子表达式Value变体表示叶子字面量。求值器eval因此天然是一个递归函数。DivideByZeroError是单元结构体unit struct它没有任何字段。答案文档专门提示这里之所以“零字段就够用”是因为除零错误不需要携带额外上下文——出错的原因本身就是完整信息。#[derive(PartialEq, Eq, Debug)]让它可以参与assert_eq!比较并能被打印调试。文件顶部有#![allow(dead_code)]练习代码中部分类型在未完成改造前可能未被全部使用这一属性允许编译器不产生未使用代码警告让学员专注练习本身。标准答案用Result重构eval完整的标准答案位于 exercise.rs 的// ANCHOR: solution段同时被 solution.md 通过 mdBook 的{{#include exercise.rs:solution}}锚点机制直接嵌入页面这种方式保证文档展示的代码与仓库中的真实源码永远一致fn eval(e: Expression) - Resulti64, DivideByZeroError { match e { Expression::Op { op, left, right } { let left eval(*left)?; let right eval(*right)?; Ok(match op { Operation::Add left right, Operation::Sub left - right, Operation::Mul left * right, Operation::Div { if right 0 { return Err(DivideByZeroError); } else { left / right } } }) } Expression::Value(v) Ok(v), } }答案文档提炼了四个核心改造点1. 返回类型改为Resulti64, DivideByZeroError函数签名从i64变为Resulti64, DivideByZeroError。这个显式的类型签名强制调用方必须处理失败的可能性——这正是 Rust 错误处理与异常机制的本质差异。课程在 result.md 中强调Result有两个变体Ok携带成功值Err携带某种错误值函数是否可能出错被直接编码在类型签名中因此“忘记处理错误”在编译层面就不可能发生。2.?运算符递归调用的错误传播答案在递归调用上使用?let left eval(*left)?;。这行代码等价于手写match eval(*left) { Ok(value) value, Err(err) return Err(err), }try.md 将这一模式总结为?把常见的match样板压缩成一个运算符——若eval返回Err函数立即把该Err原样返回给调用方若返回Ok(v)则把v解包并赋给left或right。这让错误沿调用链“透明”向上传播代码几乎和异常机制一样简洁但控制流是显式的、类型检查器可见的。3.Ok包装成功结果成功路径必须显式包装为Ok(...)。注意答案的写法Ok(match op { ... })——对Add、Sub、Mul三个分支match表达式本身求出i64再整体包进Ok只有Div分支内部需要特殊处理。对于Expression::Value(v)叶子节点则直接Ok(v)。4. 显式检查除零取代 panicOperation::Div { if right 0 { return Err(DivideByZeroError); } else { left / right } }这里用显式的right 0检查返回Err(DivideByZeroError)替换了原代码中的panic!(Cannot divide by zero!)。注意答案中?已提前把left、right解包为i64所以这里可以直接做数值比较。从语义上看return Err(...)在match分支内部提前返回控制流清晰且不依赖任何异常机制。单元测试验证错误与成功两条路径答案文档还通过{{#include exercise.rs:tests}}嵌入了配套测试exercise.rs 的// ANCHOR: tests段#[cfg(test)] mod test { use super::*; #[test] fn test_error() { assert_eq!( eval(Expression::Op { op: Operation::Div, left: Box::new(Expression::Value(99)), right: Box::new(Expression::Value(0)), }), Err(DivideByZeroError) ); } #[test] fn test_ok() { let expr Expression::Op { op: Operation::Sub, left: Box::new(Expression::Value(20)), right: Box::new(Expression::Value(10)), }; assert_eq!(eval(expr), Ok(10)); } }两个测试恰好覆盖改造后的两条关键路径test_error构造99 / 0的表达式树断言eval返回Err(DivideByZeroError)。它直接验证了本次重构的核心目标——除零不再 panic而是产生可处理的错误值。这也是DivideByZeroError必须派生PartialEq的原因assert_eq!需要比较两个错误值是否相等。test_ok构造20 - 10的表达式树断言返回Ok(10)保证正常求值路径没有被破坏同时验证了Ok包装与?解包的往返正确性。从构建配置看这个练习是作为独立的 Rust 库lib 名parser组织的Cargo.toml 声明[lib] name parser入口即exercise.rsBUILD.bazel 则定义了rust_libraryparser与rust_testparser_testsize 为small两条 Bazel 规则。课程允许你按自己熟悉的方式运行使用 Cargo在该目录执行cargo test仓库根目录有对应 Cargo.toml 工作区配置练习位于 src/error-handling/Cargo.toml使用 Bazel执行bazel test //src/error-handling:parser_test。parser依赖anyhow与thiserror两个 crate见 Cargo.toml它们对应错误处理章节后段的进阶内容anyhow.md、thiserror.md本练习刻意不引入它们让学员先用标准库类型掌握原理。为什么这样改Result与错误处理范式对比答案文档的details提示区给出了两个供讲师展开的讨论点也是理解本练习价值的钥匙其一关于单元结构体错误类型。DivideByZeroError没有任何字段就已足够因为它不需要提供除“除零发生了”之外的额外上下文。当你需要携带更多信息如哪个表达式、当时的左右操作数时再考虑为错误类型增加字段或改用更丰富的错误类型。其二关于?与异常的对比。?让错误处理“几乎和异常一样简洁但控制流是显式的”。课程在 result.md 的 “More to Explore” 中系统比较了三种范式异常机制C、Java、Python 等函数是否可能抛异常不在类型签名中可见调用方无从判断异常会展开调用栈向上传播深处产生的错误可能波及上层无关函数。错误码C、Go 等错误值与成功值分离返回语言层面可能忘记检查错误码进而访问未初始化或无效的成功值。ResultRust成功与失败都被编码进类型不先匹配解包就无法取得值unwrap之类的方法虽然可以快速写出不做健壮处理的代码但源码中始终可见“哪里跳过了应有的错误处理”。超越本练习从Result到动态错误类型掌握本练习后error.md 展示了更进一步的模式当不想为所有可能错误手写枚举时可以用std::error::Errortrait 构造 trait objectfn read_count(path: str) - Resulti32, Boxdyn Error { let mut count_str String::new(); fs::File::open(path)?.read_to_string(mut count_str)?; let count: i32 count_str.parse()?; Ok(count) }read_count会从文件操作产生std::io::Error、从字符串解析产生std::num::ParseIntError而Boxdyn Error让它们可以被统一返回。文档同时给出重要权衡装箱错误省代码但放弃了对不同错误分类处理的能力因此不适合作为库的公开 API更适合“只想把错误消息展示出来”的应用程序。若定义自定义错误类型务必实现std::error::Errortrait 以便装箱。回到本练习如果你希望在生产级库中定义更结构化的错误类型thiserror.md 提供的thiserror派生宏可以自动为错误类型生成Display与Error实现在应用层聚合错误时anyhow.md 的anyhow::Result则是更轻量的选择——这与本练习所在的 Cargo.toml 中同时声明anyhow与thiserror依赖的设计一脉相承。小结本练习的完整脉络是发现 panic 误用除零→ 用Result改造签名 → 用?简化递归错误传播 → 用Ok显式包装成功 → 用单元测试锁死错误与成功两条路径。它浓缩了 Rust 错误处理的核心哲学错误是值成功与失败都是类型的一部分编译器帮你确保每一个可能失败的地方都被显式处理。完整代码、注释与测试均可直接在 exercise.rs 中查阅讲师讨论要点见 solution.md配套概念讲解依次见 result.md、try.md 与 panics.md。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询