第六章:错误处理 —— 告别 null 和 try-catch

Unknown Author 独酌
2025-11-20 Rust, Book

作为前端,我们有两个“一生之敌”:

  1. Uncaught TypeError: Cannot read properties of undefined (访问了空值)
  2. try { ... } catch (e) { ... } (面条式错误处理)

Rust 说:我们不需要 null,也不需要 Exception (异常)。 Rust 用两个普通的 枚举 (Enum) 解决了这两个问题:OptionResult

1. Option —— 薛定谔的盒子

在 Rust 里,变量永远不可能是 null。如果你想要一个“可能为空”的值,你必须把它装进 Option 盒子。

Option 枚举长这样(标准库内置):

1
2
3
4
enum Option<T> {
Some(T), // 盒子里有东西
None, // 盒子是空的 (代替 null)
}

实战对比:

JS (TypeScript):

1
2
3
4
5
6
function findUser(id: number): string | null {
if (id === 1) return "Gemini";
return null;
}
const user = findUser(2);
// 你可能忘了检查 null,直接 user.toUpperCase() -> 💥 报错

Rust:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn find_user(id: i32) -> Option<String> {
if id == 1 {
Some(String::from("Gemini"))
} else {
None
}
}

fn main() {
let user_box = find_user(2);

// ❌ 报错:你不能直接把 Option<String> 当 String 用
// println!("User: {}", user_box);

// ✅ 你必须先"拆包" (使用 match)
match user_box {
Some(name) => println!("找到了用户: {}", name),
None => println!("查无此人"),
}
}

Rust 强迫你在使用数据之前,必须先检查盒子是不是空的。这就从根源上消灭了 undefined 报错。

2. Result<T, E> —— 成功还是失败?

对于可能报错的操作(比如读取文件、网络请求),Rust 使用 Result。它也是个枚举:

1
2
3
4
enum Result<T, E> {
Ok(T), // 成功,得到数据 T
Err(E), // 失败,得到错误 E
}

这比 try-catch 清晰得多,因为错误处理变成了正常的逻辑流程,而不是“异常跳转”。

一个最受 Rust 开发者喜爱的符号:?

如果每一个 Result 都要写 match 来处理,代码会很啰嗦。Rust 提供了一个神级语法糖:问号操作符 (?)。

它的意思是:“如果是 Error,直接把错误抛给上一层(return Err);如果是 Ok,把里面的数据取出来给我。”

这非常像 JS 中的 await,能把异步代码写成同步的样子;? 能把错误处理代码写成线性的样子。

1
2
3
4
5
6
7
8
9
10
11
12
// 假设 read_file 返回 Result<String, Error>
fn read_username_from_file() -> Result<String, std::io::Error> {
// 打开文件,如果失败直接 return Err
let mut f = File::open("hello.txt")?;

let mut s = String::new();
// 读取内容,如果失败直接 return Err
f.read_to_string(&mut s)?;

// 如果一路顺利,返回 Ok
Ok(s)
}

看到那些 ? 了吗?它帮我们省去了无数的 if err != nil 或者 try-catch

🧠 随堂小测验

假设我们要写一个除法函数 divide。 大家都知道,除数不能为 0。如果用户传了 0,我们应该报错。

请从下面三个选项中,选出最符合 Rust 风格的函数签名(Function Signature):

A. fn divide(a: i32, b: i32) -> i32 (如果除以0,让程序崩溃 panic)

B. fn divide(a: i32, b: i32) -> Option<i32> (如果除以0,返回 None;否则返回 Some结果)

C. fn divide(a: i32, b: i32) -> Result<i32, String> (如果除以0,返回 Err(“不能除以0”);否则返回 Ok结果)

(提示:B 和 C 都是合法的 Rust 风格,但如果是为了告诉调用者为什么失败,哪个更好?)

🎉 C 是最佳答案。

为什么选 C (Result) 而不是 B (Option)?
虽然用 Option (返回 None) 来表示“除以零”也是合法的,但 Result 提供了一个关键的优势:它可以携带错误信息。

  • Option<T>:适用于“有”或“没有”的场景(比如查找数组里的元素,没找到就是 None,不需要解释为什么)。
  • Result<T, E>:适用于“成功”或“失败”的场景,特别是当你需要告诉调用者为什么失败时(比如“除数不能为零”、“文件不存在”、“权限不足”)。

这样调用者不仅知道出错了,还能把错误信息打印日志或者展示给用户。

我们已经攻克了 Rust 的“三大难关”:所有权、借用、生命周期(其实在借用里我们隐式地学到了,不用太担心),以及刚刚搞定的错误处理

现在,我们要进入 第三阶段:抽象与并发。这一阶段会让你的代码变得真正“高级”起来。

独酌

独酌

小镇码农

相关文章