CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Rust 语言入门/课时/错误处理
RawBlame
课时目录12 节

Rust 语言入门

  1. 01Rust 是什么
  2. 02基础语法
  3. 03所有权与借用
  4. 04生命周期
  5. 05结构体与枚举
  6. 06错误处理
  7. 07Trait 与泛型
  8. 08集合类型
  9. 09并发与 async
  10. 10宏与元编程
  11. 11Web 与生态
  12. 12综合练习

错误处理

Option、Result、? 运算符、自定义错误。

30 分钟

错误处理

这一课你将能做到

  1. 区分 panic! 与 Result 的适用边界,杜绝「图省事一路 unwrap」
  2. 熟练使用 ? 运算符传播错误,实现自定义错误类型并完成 From 转换
  3. 用 Option/Result 组合子链式处理「可能失败」的值

为什么要学

Java 用异常处理错误:try-catch 隐藏在调用栈里,方法签名看不出会不会炸,checked exception 还常年被绕过。Rust 把错误写进函数签名——Result<T, E> 类型上声明「我可能失败,失败长这样」,编译器强制调用方表态。

对高性能后端这有两个实际收益:错误路径无异常展开的开销(返回值即错误),以及读代码时一眼看清所有失败分支。? 运算符让错误传播从 Java 的层层 try-catch 变成一个字符。

先建立图像

Java:                      Rust:
int parse(String s)         fn parse(s: &str) -> Result<u32, ParseIntError>
throws NumberFormatException
                            调用方三选一(编译器强制):
try-catch 掩盖路径          ① ? 直接上抛
                            ② match 拆开处理
                            ③ unwrap/expect 显式声明「这里不可能失败」

两个错误世界的分工:

  • panic!:不可恢复的 bug(数组越界、断言失败),对标 Java 的 RuntimeException 直接炸进程/线程
  • Result<T, E>:可预期的失败(解析失败、文件不存在、上游超时),对标 checked exception 但更诚实

跟着做

步骤 1:Result 的基本形状

cargo new err-demo
cd err-demo
use std::num::ParseIntError;

fn parse_time_limit(raw: &str) -> Result<u32, ParseIntError> {
    raw.trim().parse::<u32>()
}

fn main() {
    match parse_time_limit(" 1000 ") {
        Ok(ms) => println!("时限 {ms} ms"),
        Err(e) => println!("解析失败:{e}"),
    }

    match parse_time_limit("abc") {
        Ok(ms) => println!("时限 {ms} ms"),
        Err(e) => println!("解析失败:{e}"),
    }
}

Result<u32, ParseIntError>:成功带 u32,失败带错误详情。" 1000 ".trim().parse() 是标准库字符串转数字的惯用链。

步骤 2:? 运算符——错误传播的最短路径

fn read_limit(line: &str) -> Result<u32, String> {
    let (_, raw) = line.split_once('=').ok_or("缺少 '=' 分隔符")?;
    //                                  ^ 拆不出 '=' 就提前返回 Err(&str 自动转 String)
    let limit = raw.trim().parse::<u32>().map_err(|e| e.to_string())?;
    //                                     ^ 解析失败自动 return Err
    Ok(limit)
}

fn main() {
    println!("{:?}", read_limit("time_limit = 2000"));
    println!("{:?}", read_limit("garbage-line"));
}

? 做三件事:检查 Result、失败则携带错误提前 return、成功则取出 Ok 里的值。一个字符顶掉 Java 的三层 try-catch。注意 只能在返回 Result 的函数里用。

?

步骤 3:自定义错误类型——用 enum 归类失败

use std::fmt;

#[derive(Debug)]
enum ConfigError {
    MissingField(&'static str),
    BadValue { field: &'static str, raw: String },
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::MissingField(name) => write!(f, "缺少字段:{name}"),
            ConfigError::BadValue { field, raw } => write!(f, "字段 {field} 的值非法:{raw}"),
        }
    }
}

impl std::error::Error for ConfigError {}   // 接入标准错误体系,使其可与 ? 兼容

fn parse_limit(raw: &str) -> Result<u32, ConfigError> {
    if raw.is_empty() {
        return Err(ConfigError::MissingField("time_limit"));
    }
    raw.trim().parse::<u32>()
        .map_err(|_| ConfigError::BadValue { field: "time_limit", raw: raw.to_string() })
}

fn main() -> Result<(), ConfigError> {
    let limit = parse_limit("1000")?;
    println!("时限 {limit} ms");
    let bad = parse_limit("");
    println!("{bad:?} / {bad}");
    Ok(())
}

自定义错误三件套:enum 列出失败种类、实现 Display 给人看、实现 Error trait 接入生态(之后可被 Box<dyn Error> 收编、可被 ? 转换)。社区常直接用 thiserror 宏生成这三件套,思路完全一致(第 10 课)。

步骤 4:跨错误类型的自动转换

use std::num::ParseIntError;

// Box<dyn Error> 能收编所有实现了 Error 的类型;? 自动用 From 转换
fn load_config(text: &str) -> Result<u32, Box<dyn std::error::Error>> {
    let first = text.lines().next().ok_or("空配置")?;   // &str 自动转 Box<dyn Error>
    let (_, value) = first.split_once('=').ok_or("缺少 = 分隔")?;
    let limit: u32 = value.trim().parse()?;             // ParseIntError 自动转 Box
    Ok(limit)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let limit = load_config("time_limit=1500")?;
    println!("加载成功:{limit} ms");
    Ok(())
}

main 也能返回 Result:Err 时程序以非零码退出并打印错误,CLI 工具的标准姿势。

步骤 5:Option 与 Result 的组合子工具箱

fn main() {
    let score: Option<u32> = Some(88);
    let missing: Option<u32> = None;

    // map:对 Some 里的值变形,None 原样穿透
    let doubled = score.map(|s| s * 2);
    println!("{doubled:?}");

    // unwrap_or / unwrap_or_else / unwrap_or_default:缺省值三兄弟
    println!("{}", missing.unwrap_or(0));
    println!("{}", missing.unwrap_or_else(|| 60));   // 惰性求值
    println!("{:?}", missing.unwrap_or_default());

    // ok_or:Option 转 Result;and_then:链式处理可能失败的步骤
    let ok: Result<u32, &str> = score.ok_or("缺考");
    println!("{ok:?}");
    let pass = score.and_then(|s| if s >= 60 { Some("及格") } else { None });
    println!("{pass:?}");
}

组合子是「不 panic 地处理缺失」的优雅路径;unwrap() 只留给测试和可证明安全的场景。

讲清楚要点

  1. 错误在签名里:Result<T, E> 让失败路径成为类型契约的一部分,对标 Java 方法不写 throws 但运行时照样抛的隐性风险。
  2. ? 的完整语义:失败提前返回 + 错误类型自动 From 转换 + 成功解包取值;只在返回 Result(或 Option)的函数中可用。
  3. 错误类型设计原则:业务层用自定义 enum(调用方好 match),工具层/原型用 Box<dyn Error>(省事),二进制入口用 anyhow 风格(只需展示无需处理)。
  4. panic 的纪律:只用于 bug(不变量被破坏);用户输入、外部 IO 一律 Result。库代码 panic 是接口事故。
  5. #[must_use] 强制表态:Result 带有 must_use 属性,返回值被忽略时编译器警告——「调用了但没处理错误」在 Rust 里藏不住。

你可能会懵的地方

  • ? 和 Java 的 throw 哪个开销大? ? 是零成本:失败时就是一次提前 return,无栈展开、无捕获表。Java 异常首次抛出需要填充栈轨迹,热路径上的异常是真实的性能税。

  • Option 和 Result 为什么不合并成一个? Option 表达「没有值」(不是错误),Result 表达「失败了且带原因」。ok_or / ok() 两者互转;语义分开让函数签名更诚实。

  • 什么时候该 panic? 测试断言、程序自身不变量被破坏(如配置加载在启动阶段失败)、示例代码。服务运行期的外部输入永远走 Result。

  • 为什么 ? 需要错误类型实现 From? ? 遇到 Err(E1) 而函数返回 Err(E2) 时,自动调 From::from(E1) 转换。这是「底层错误翻译成业务错误」的机制,免掉手写 match 转发。

  • Box<dyn Error> 和自定义 enum 怎么选? CLI 和原型直接 Box<dyn Error> 省事;库和核心业务定义 enum,调用方才能精确 match 分支处理,而不是拿字符串猜原因。

错法对照

错法问题正解
业务代码里 unwrap() 一把梭None/Err 直接 panic?、unwrap_or、expect("明确原因")
在返回 () 的函数里用 ?error[E0277] 不支持函数签名改为 -> Result<T, E>
自定义错误没实现 Error/Display? 转换、打印都不工作实现 Display + impl Error for ...
用 panic 处理用户输入输入不可控必炸全部走 Result,错误信息返回给调用方
忽略返回的 Result(如 file.write(...);)warning: unused must_uselet _ = ... 显式丢弃,或 ? 上抛,或 .ok()

卡点急救

  1. error[E0277] the ? operator can only be used...:所在函数必须返回 Result/Option。改签名,或在末尾 Ok(...) 收尾;小工具直接让 main 返回 Result<(), Box<dyn Error>>。
  2. 错误类型转不过去(? 报 From 不匹配):给目标错误实现 impl From<SourceErr> for TargetErr,或先用 map_err 手动翻译再 ?。
  3. 调试时想快速看错误全貌:临时 .unwrap() 定位,修复后换回 expect("上下文说明");用 dbg!(&result) 打印整棵 Result 而不触发 panic。

面试常问

  • Rust 的 Result 和 Java 异常体系的对比? Result 把失败写进类型签名,调用方被编译器强制处理,错误路径零展开开销;Java 异常是隐藏控制流,checked exception 常被吞掉,热路径抛异常有栈轨迹成本。Rust 用「值」建模错误,Java 用「跳转」建模错误。

  • ? 运算符做了什么? 三合一:Err 时提前 return;返回类型不同时经 From 自动转换;Ok 时解包取值。它是 Rust 错误传播链可读性远超 try-catch 嵌套的关键。

  • panic 与 Result 的选型原则? panic 留给「程序 bug / 不变量被破坏」,表现为快速失败;一切依赖外部输入(用户、文件、网络)的失败走 Result。库的公共 API 里 panic 是事故,panic 边界应收敛为 Result。

自测三问

  1. ? 遇到 Err(E1) 且函数返回 Result<T, E2> 时发生了什么?
  2. 为什么自定义错误要同时实现 Display 和 Error?
  3. map、and_then、unwrap_or_else 各适合哪类链式处理?

小结一句话

错误是值不是异常:签名即契约、? 即传播、match 即处理——Rust 把 Java 里最容易糊弄过去的错误处理变成了编译器盯着的必修课。

延伸

  • The Rust Book ch09:错误处理
  • 下一课:Trait 与泛型——Rust 的「接口」与「模板」,静态分发碾压反射的性能来源。
加载本节练习…
← 上一课结构体与枚举下一课 →Trait 与泛型