用类型表达:没有 null,也没有异常

Result 与 ?:没有异常怎么活

它在解决什么

Option 回答「可能没有」。但真实代码里更常见的是 「可能失败,而且失败有原因」—— 文件不存在、端口超范围、JSON 格式不对。 这些情况下调用方需要知道的不只是「没成功」,还有「为什么」, 因为它要据此决定给用户什么提示、要不要重试。

Rust 的答案是 Result<T, E>,同样是一个普通 enum:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

它和异常最大的区别是:错误是一个值,不是一条控制流。 函数签名里写着它会失败,编译器盯着你处理它, 不存在「某个三层之外的调用悄悄抛了个东西上来」这回事。

丢掉「为什么」的代价

同一个函数的两个版本并排看最清楚:

fn parse_port(s: &str) -> Result<u16, PortError>   // Ok(8080) / Err(NotANumber) / Err(OutOfRange(99999))
fn parse_port(s: &str) -> Option<u16>              // Some(8080) / None / None

两边都编译通过,差别全在输出上:Option 版本把「不是数字」和「超出范围」 压成了同一个 None。调用方于是没法区分「用户打错了」和 「用户要了一个不存在的端口」—— 而那正是它用来决定提示文案的信息。

⇒ 判据很简单:调用方会不会因为失败原因不同而做不同的事。 会,就用 Result;不会(比如查字典没查到),Option 更省事。

? 做的事比你以为的多

? 最广为人知的是「是 Err 就提前返回」。但它还做了第二件事, 而那一件才是它在真实项目里不可替代的原因。

一、它要求当前函数能装下那个 Err

在普通的 main 里用 ? 是 E0277 —— 因为 ? 的展开里有一个 return Err(...),而 fn main() 返回 ()。

⭐ 给 main 加上返回类型就行:

fn main() -> Result<(), ParseIntError> {
    let n = parse("42")?;
    println!("{n}");
    Ok(())
}

写小工具时这是最省事的写法,不用为了用 ? 专门拆一个函数出来。

⚠️ 这一段没有示例盯着(我在本机实测了一次):main 返回 Err 时, 进程以退出码 1 结束,并把 Err 的 Debug 输出打到 stderr 上, 形如 Error: "配置文件不存在"。Ok 之前打印的东西照常留在 stdout。 这个行为对写 CLI 很要紧 —— 它让「返回 Err」和 shell 的 && / || 天然对上。

二、它顺手把错误类型翻译了

这才是 ? 最值钱的一半。看这段:

fn read(line: &str) -> Result<u16, ConfigError> {
    let value = line.split('=').nth(1).ok_or(ConfigError::Missing)?;
    let n: u16 = value.trim().parse()?;    // ← 这里是 ParseIntError
    Ok(n)
}

parse() 给的是 ParseIntError,而函数声明要返回 ConfigError。 它们之间是靠 ? 悄悄调的一次 From::from 接上的 —— 删掉那个 impl From 就是 E0277。

⇒ 所以给自己的错误类型写几个 From impl,是让 ? 在整个项目里顺畅起来的 前置动作。写了之后,每一层只管声明自己那层的错误类型,翻译由 ? 负责。

📌 实践中这件事通常交给 thiserror 这类库去生成,但机制就是上面这个。

Option 和 Result 可以互相转

两边的转换是双向的,而且名字很好记:

方向 写法 在做什么
Option → Result ok_or(e) / ok_or_else(|| e) 给 None 补一个理由
Result → Option .ok() 把 Err 里的信息扔掉

ok_or_else 那条示例里, 直接把 Option 返回出去就是 E0308 —— 它俩不是一个类型。

⚠️ 用 ok_or_else 而不是 ok_or:后者的参数是个值, 无论成功失败都会先构造出来。错误信息里带 format! 的时候这个差别不小。

两件真实项目里必然撞到的事

一串 Result 能直接 collect 成一个 Result

这条很多人没想到:

fn parse_all(items: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    items.iter().map(|s| s.parse::<i32>()).collect()
}

collect 看返回类型决定收成什么。收进 Result<Vec<_>, _> 时, 它遇到第一个 Err 就停下并返回那个 Err; 收进 Vec<Result<_, _>> 时一个不落全收着。

两种都有用 —— 前者是「全都得成功」,后者是「告诉我哪些失败了」。 选哪个只取决于你写的返回类型。

多种错误怎么统一

一个函数里往往会冒出好几种错误类型。最省事的办法是 Box<dyn Error>:

fn read_port(s: &str) -> Result<u16, Box<dyn Error>> {
    let value = s.split('=').nth(1).ok_or("缺少等号")?;   // 这里是 &str
    let n: u16 = value.trim().parse()?;                   // 这里是 ParseIntError
    Ok(n)
}

两种完全不同来源的错误都能被 ? 塞进去,因为标准库为 Box<dyn Error> 实现了足够多的 From。

⚖️ 代价是调用方没法再 match 出具体是哪一种(换成具体类型就编译不过)。 ⇒ 判据:写工具和原型用 Box<dyn Error>,写给别人调的库定自己的错误 enum。

什么时候该 panic 而不是返回 Result

不是所有失败都该走 Result。分界线是:

  • Result —— 调用方可能想处理它。文件不存在、输入格式不对、网络超时。
  • panic —— 说明程序里有 bug,继续执行只会让问题更难查。 数组越界、除零、违反了函数文档里写明的前提。

📌 一个实用的换算:如果这个失败是「外界给的东西不对」,用 Result; 如果是「我自己写错了」,panic。

而 unwrap / expect 就是在说「我断定这里不会失败」—— 跟 Option 那篇一样,要用就用 expect 把理由写进去。

下一步

Option<T>、Result<T, E>、Vec<T> —— 这一章一路在用泛型,却一直没解释它。 见《泛型、trait 与 dyn》:静态分发和动态分发的区别, 以及「什么时候不得不用 dyn」那个唯一的硬理由。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

`Result` 就是带上了「为什么」的 `Option`

#[derive(Debug)]
enum PortError {
    NotANumber,
    OutOfRange(u32),
}

fn parse_port(s: &str) -> Result<u16, PortError> {
    let n: u32 = s.parse().map_err(|_| PortError::NotANumber)?;
    if n > 65535 {
        return Err(PortError::OutOfRange(n));
    }
    Ok(n as u16)
}

fn main() {
    println!("{:?}", parse_port("8080"));
    println!("{:?}", parse_port("abc"));
    println!("{:?}", parse_port("99999"));
}

编译通过 · 输出 "Ok(8080)\nErr(NotANumber)\nErr(OutOfRange(99999))\n"

对照:改成返回 `Option<u16>`
fn parse_port(s: &str) -> Option<u16> {
    let n: u32 = s.parse().ok()?;
    if n > 65535 {
        return None;
    }
    Some(n as u16)
}

fn main() {
    println!("{:?}", parse_port("8080"));
    println!("{:?}", parse_port("abc"));
    println!("{:?}", parse_port("99999"));
}

编译通过,输出:"Some(8080)\nNone\nNone\n"

⭐ 两边都编译通过,差别全在输出上:`Option` 版本把「不是数字」和「超出范围」**压成了同一个 `None`**。调用方于是没法区分「用户打错了」和「用户想要一个不存在的端口」—— 而那正是它需要用来决定给什么提示的信息。

`?` 会自动转换错误类型 —— 靠的是 `From`

use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    BadNumber(ParseIntError),
    Missing,
}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::BadNumber(e)
    }
}

fn read(line: &str) -> Result<u16, ConfigError> {
    let value = line.split('=').nth(1).ok_or(ConfigError::Missing)?;
    let n: u16 = value.trim().parse()?;
    Ok(n)
}

fn kind(r: Result<u16, ConfigError>) -> String {
    match r {
        Ok(n) => format!("ok {n}"),
        Err(ConfigError::Missing) => String::from("缺少等号"),
        Err(ConfigError::BadNumber(_)) => String::from("不是数字"),
    }
}

fn main() {
    println!(
        "{} / {} / {}",
        kind(read("port = 80")),
        kind(read("port")),
        kind(read("port = x"))
    );
}

编译通过 · 输出 "ok 80 / 缺少等号 / 不是数字\n"

对照:删掉 `impl From<ParseIntError> for ConfigError`
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    BadNumber(ParseIntError),
    Missing,
}

fn read(line: &str) -> Result<u16, ConfigError> {
    let value = line.split('=').nth(1).ok_or(ConfigError::Missing)?;
    let n: u16 = value.trim().parse()?;
    Ok(n)
}

fn kind(r: Result<u16, ConfigError>) -> String {
    match r {
        Ok(n) => format!("ok {n}"),
        Err(ConfigError::Missing) => String::from("缺少等号"),
        Err(ConfigError::BadNumber(_)) => String::from("不是数字"),
    }
}

fn main() {
    println!(
        "{} / {} / {}",
        kind(read("port = 80")),
        kind(read("port")),
        kind(read("port = x"))
    );
}

编译不过:error[E0271]

`value.trim().parse()` 给的是 `ParseIntError`,而函数要返回 `ConfigError` —— `?` 在中间悄悄调了一次 `From::from`。删掉那个 `impl` 就是对照项的 `E0277`。⇒ 这是 `?` 最值钱的一半:**它不只是提前返回,还负责把下层的错误翻译成本层的错误类型**。

`?` 只能用在返回 `Result` 的函数里 —— `main` 也可以是

use std::num::ParseIntError;

fn parse(s: &str) -> Result<i32, ParseIntError> {
    s.parse()
}

fn main() {
    let n = parse("42")?;
    println!("{n}");
}

编译不过 · error[E0277]

rustc 原文
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
  --> result-question-mark-needs-result-fn.rs:8:24
   |
 7 | fn main() {
   | --------- this function should return `Result` or `Option` to accept `?`
 8 |     let n = parse("42")?;
   |                        ^ cannot use the `?` operator in a function that returns `()`
   |
help: consider adding return type
   |
 7 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
 8 |     let n = parse("42")?;
 9 |     println!("{n}");
10 +     Ok(())
   |
对照:给 `main` 加上返回类型,并在末尾 `Ok(())`
use std::num::ParseIntError;

fn parse(s: &str) -> Result<i32, ParseIntError> {
    s.parse()
}

fn main() -> Result<(), ParseIntError> {
    let n = parse("42")?;
    println!("{n}");
    Ok(())
}

编译通过,输出:"42\n"

`?` 的展开里有一个 `return Err(...)`,所以它要求当前函数的返回类型能装得下那个 `Err`。⭐ **`main` 也可以返回 `Result`** —— 这是写小工具时最省事的写法,不用为了用 `?` 专门拆一个函数出来。

`Option` 转 `Result`:`ok_or` 就是在补「为什么」

fn get(k: &str) -> Option<&'static str> {
    if k == "name" { Some("ann") } else { None }
}

fn need(k: &str) -> Result<&'static str, String> {
    get(k).ok_or_else(|| format!("缺少配置项 {k}"))
}

fn main() {
    println!("{:?}", need("name"));
    println!("{:?}", need("port"));
}

编译通过 · 输出 "Ok(\"ann\")\nErr(\"缺少配置项 port\")\n"

对照:直接把 `get(k)` 返回出去
fn get(k: &str) -> Option<&'static str> {
    if k == "name" { Some("ann") } else { None }
}

fn need(k: &str) -> Result<&'static str, String> {
    get(k)
}

fn main() {
    println!("{:?}", need("name"));
    println!("{:?}", need("port"));
}

编译不过:error[E0308]

`Option` 和 `Result` 之间的转换是双向的:`ok_or` / `ok_or_else` 给 `None` 补一个理由变成 `Err`,反过来 `.ok()` 把 `Err` 里的信息扔掉变成 `None`。⚠️ 用 `ok_or_else` 而不是 `ok_or`:后者**无论成功失败都会先把那个 `String` 构造出来**。

一串 `Result` 能直接 `collect` 成一个 `Result`

use std::num::ParseIntError;

fn parse_all(items: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    items.iter().map(|s| s.parse::<i32>()).collect()
}

fn main() {
    println!("{:?}", parse_all(&["1", "2", "3"]));
    println!("{:?}", parse_all(&["1", "x", "3"]).is_err());
}

编译通过 · 输出 "Ok([1, 2, 3])\ntrue\n"

对照:把返回类型改成 `Vec<Result<i32, _>>`(收成一串结果)
use std::num::ParseIntError;

fn parse_all(items: &[&str]) -> Vec<Result<i32, ParseIntError>> {
    items.iter().map(|s| s.parse::<i32>()).collect()
}

fn main() {
    println!("{:?}", parse_all(&["1", "2", "3"]).len());
    println!("{:?}", parse_all(&["1", "x", "3"]).len());
}

编译通过,输出:"3\n3\n"

⭐ 这条是很多人没想到的:`collect` 看返回类型决定收成什么。收进 `Result<Vec<_>, _>` 时它**遇到第一个 `Err` 就停下并返回那个 `Err`**;收进 `Vec<Result<_, _>>` 时它一个不落全收着。两种都有用,区别只在你写的返回类型。

`Box<dyn Error>`:懒人版的错误统一

use std::error::Error;

fn read_port(s: &str) -> Result<u16, Box<dyn Error>> {
    let value = s.split('=').nth(1).ok_or("缺少等号")?;
    let n: u16 = value.trim().parse()?;
    Ok(n)
}

fn main() {
    println!("{:?}", read_port("port = 80"));
    println!("{}", read_port("port").unwrap_err());
    println!("{}", read_port("port = x").is_err());
}

编译通过 · 输出 "Ok(80)\n缺少等号\ntrue\n"

对照:把错误类型换成具体的 `ParseIntError`
fn read_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    let value = s.split('=').nth(1).ok_or("缺少等号")?;
    let n: u16 = value.trim().parse()?;
    Ok(n)
}

fn main() {
    println!("{:?}", read_port("port = 80"));
    println!("{}", read_port("port").unwrap_err());
    println!("{}", read_port("port = x").is_err());
}

编译不过:error[E0277]

两种不同来源的错误(一个 `&str`、一个 `ParseIntError`)都能被 `?` 塞进 `Box<dyn Error>`,因为标准库为它实现了足够多的 `From`。⚖️ 代价是调用方**没法再 `match` 出具体是哪种错** —— 所以它适合写工具和原型,而对外的库该定自己的错误类型。