用类型表达:没有 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() 返回 ()。
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` 出具体是哪种错** —— 所以它适合写工具和原型,而对外的库该定自己的错误类型。