Rust 入门:编译器是你最严格的朋友 书架

第 09 章 / 共 12 章

第九章 · 错误处理:没有异常的世界

异常的问题:一条看不见的密道

先想清楚 Rust 在反对什么。Java、Python 的异常机制(exception)是这样的:函数深处出了错,扔出一个异常,它沿着调用链一路向上飞,撞见第一个接住它的 try/catch 为止。问题是——看函数签名,你看不出它会扔什么。一段岁月静好的代码,可能从任何一行蹦出一个异常跳到十万八千里外。错误处理走的是一条看不见的控制流密道。

Rust 的答案是把密道变成明路:错误不是飞出来的,是退回来的——它是一个普通的返回值。类型就是上一章见过的枚举,叫 Result

enum Result<T, E> {
    Ok(T),   // 成功,装着结果
    Err(E),  // 失败,装着错误原因
}

读文件的函数签名是 fn read(...) -> Result<String, io::Error>——「可能失败」写在签名里,白纸黑字。想拿到里面的 String,就必须处理 Err 分支,和 Option 一个道理:失败不是暗坑,是明牌

问号运算符:一个字符的错误流水线

凡事都手写 match 太啰嗦。真实代码长这样:

use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    let text = fs::read_to_string("config.toml")?;
    let trimmed = text.trim().to_string();
    Ok(trimmed)
}

那个 ? 是 Rust 里最值钱的标点。它跟在 Result 后面,意思是:如果是 Ok,把里面的值拆出来继续;如果是 Err,立刻带着这个错误从当前函数退出去,交还给调用者。一行顶一个 match。而且因为函数的返回类型本身就是 Result,「把错误退出去」和「正常返回」走的是同一条类型通道,编译器全程盯着,类型对不上就报错。

对比异常机制,? 的妙处是:每一个可能的失败点在代码里都看得见——哪个调用会失败、失败后错误往哪走,顺着问号一路读上去就行,没有密道。这也是 Rust 社区那句口号的含义:errors are values(错误是值)。值就要走值的规矩:显式传递、类型检查、不许隐身。

类比:异常像火警——警铃一响全楼的人都跑,你事后才知道是三楼微波炉着了;Result + ? 像快递签收链——每个环节要么签收要么写明「破损退回」,谁经手、在哪退的,面单上一清二楚。

那 panic 算什么?

Rust 也有「炸了」的时候,叫 panic——程序中止当前线程并展开清理(或按配置直接退出)。数组越界访问就会 panic,第三章的整数溢出(调试版)也会 panic。它和 Result 的分工是:

标准库里两个和 panic 直接相关的常用方法:.unwrap()——「是 Ok 就给我值,是 Err 就原地 panic」;.expect("理由")——同款,但 panic 时带上你写的说明。它们适合两种场合:写原型、写测试图快;以及你能论证这里绝不可能失败(比如解析一个你刚亲手拼好的字符串)。库代码里乱用 unwrap 是坏习惯——等于替你的用户决定「这种失败不配被处理」。

一句话分场景:「这事可能会失败」→ 返回 Result;「这事理论上不会失败,失败了是我代码的 bug」→ panic 也行;「我懒得处理,先跑起来再说」→ unwrap,但记得这是欠下的债。

手感小结

Rust 日常代码的错误处理节奏是这样的:底层函数如实返回 Result;中间层用 ? 把处理不了的错误往上递;到了最顶层(main、请求处理器)再 match 一把,决定是记日志、给用户报错还是兜底重试。错误从产生到最终处理,整条链路类型可查、路径可见。配上上一章的 Option——「没有」用 Option,「失败」用 Result——你已经掌握了 Rust 程序里最常打的两种明牌。下一章看容器:Vec、HashMap 和那个让循环写法脱胎换骨的迭代器。

Rust 入门:编译器是你最严格的朋友 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校