首页 / Rust 入门教程 / ? 运算符简化错误处理

Rust 入门教程

? 运算符简化错误处理

本教程共 78 篇 · 第 41 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

RustRust 入门教程问号运算符错误处理Result传播错误FromOption

本节目标:用 ? 运算符一行替代几行 match 错误传播,理解它如何自动向上返回并转换错误类型,以及用在 Option 和 main 上的注意点。

上一章末尾那串 match 错误传播,正确但啰嗦。Rust 提供一个 ? 运算符,专门用来给错误传播”减肥”。它是日常写代码最高频的语法之一,几乎每个返回 Result 的函数里都能见到它的身影。

你可以把 ? 理解成”提前返回错误”的语法糖:它不改变控制流的本质,只是把”出错就 return”这件事压缩成了一个符号。写原型时你用 unwrap 一路到底,等正式写业务逻辑,几乎总是要把 unwrap 换成 ?——这样既不会让程序崩溃,又能把错误顺手交给上层决定怎么处理。

1-1 ? 等价于 error 分支的 return

把上一章的传播函数用 ? 改写,效果立竿见影:

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

代码少了一大半。这里的 ? 做的工作,等价于:

let mut f = match File::open("hello.txt") {
    Ok(file) => file,
    Err(e) => return Err(e), // 出错就立刻把错误返回
};

也就是说:? 加在一个返回 Result 的表达式后面,如果是 Ok(T),就把里面的 T 取出来接着用;如果是 Err(E),就立刻 return Err(E),把错误往上传。它天生就是为”传播错误”而生的。

还能链式连写,更短:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

File::open 出错就返回,没出错就把 Ok 里的句柄拿来调 read_to_string,再对它也用 ?。一行顶之前好几行。

1-2 ? 还能自动转换错误类型

? 比等价的 match 还强一点:它会自动做错误类型转换。原理是标准库的 From trait——只要目标错误类型实现了 From<当前错误类型>? 就会隐式调用转换。

use std::fs::File;
use std::error::Error;

fn open_file() -> Result<File, Box<dyn Error>> {
    let mut f = File::open("hello.txt")?;
    Ok(f)
}

File::open 出错时返回的是 std::io::Error,但函数声明的错误类型是 Box<dyn std::error::Error>(一个能装下几乎所有错误的通用 trait 对象)。? 发现两者类型不符,就通过 From 自动把 io::Error 转成了 Box<dyn Error>,于是返回通畅。

这种转换极实用:你可以定义一个”大而全”的错误类型,只要为各种子错误实现 From? 就会自动归拢,不用手写一堆转换。第 47 章会专门讲 From trait。

1-3 ? 也能用于 Option

? 不只服务 Result。用在 Option 上时,遇到 None 就直接返回 None

fn first(arr: &[i32]) -> Option<&i32> {
    let v = arr.get(0)?;
    Some(v)
}

arr.get(0) 返回 Option<&i32>。是 None 就立刻返回 None;是 Some(v) 就把 v 取出。更常见的是链式里提前返回:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

.next() 返回 Option:是 None 直接整体返回 None,不再继续;是 Some 才接着调 .chars().last()

1-4 新手常踩的坑

初学者最容易写出这种编不过的代码:

fn first(arr: &[i32]) -> Option<&i32> {
    arr.get(0)? // 报错
}

报错原因:? 只能把错误值直接返回,正确值不能。上面函数返回类型是 Option<&i32>arr.get(0)? 在成功时取出的是 &i32,不是 Option,类型对不上。正确写法是 let v = arr.get(0)?; 然后返回 Some(v),或者干脆 arr.get(0) 直接返回。

Warning

记住 ? 的两种合法形态:一种是 let v = xxx()?; 接住正确值;一种是 xxx()?.yyy()?; 链式调用。它不能直接作为”裸返回值”甩出去正确值。

1-5 能在 main 里用 ? 吗

默认 main 函数返回 ()? 要求函数返回 ResultOption(或实现了 FromResidual 的类型),所以直接写会报错:

fn main() {
    let f = File::open("hello.txt")?; // 报错:main 返回 ()
}

解决方法是给 main 换个返回类型:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;
    Ok(())
}

现在 main 返回 Result<(), Box<dyn Error>>? 就能用了。结尾用 Ok(()) 表示正常退出。这里又一次见到 Box<dyn Error>,因为它能兜住几乎所有标准库错误,最适合当 main 的统一错误出口。

Note

历史上有个 try! 宏是 ? 的前身(Rust 1.13 之前),功能类似但更啰嗦。现在一律用 ?,别再碰 try!

1-6 把 ? 完全展开成 match,看清它在做什么

? 用着省事,但初学者最好先能手写它的等价形式,才知道边界在哪。对 Result 来说,? 大致等于下面这段:

// 表达式: get_value()?
// 等价于:
match get_value() {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

注意两个关键点:第一,? 在出错分支直接 return,所以”能写 ? 的函数”其返回类型必须能装下这个错误——也就是返回 ResultOption。第二,它内部调用了 From::from(e),这就是为什么不同错误类型能被自动转成你函数声明的错误类型。

Option 而言,? 的展开是:

// x?
// 等价于:
match x {
    Some(v) => v,
    None => return None,
}
Warning

? 只能出现在返回 Result/Option 的函数体内。把 ? 写在返回 () 的普通函数里会编译失败——编译器会明确告诉你”这里不能 early return 一个错误”。

1-7 再练几个 ? 的真实场景

? 用到真实代码里,感受一下它如何消掉样板。比如读一个配置文件、解析成数字、再算点什么,三步都可能失败:

use std::fs;
use std::num::ParseIntError;

fn total() -> Result<i32, Box<dyn std::error::Error>> {
    let raw = fs::read_to_string("nums.txt")?;      // IO 可能失败
    let n: i32 = raw.trim().parse()?;               // 解析可能失败
    Ok(n * 2)
}

注意两处 ? 的错误类型不同(io::ErrorParseIntError),却都能自动 FromBox<dyn Error>,所以同一个返回类型装得下。换成 match 的话,这里要写两层 match 加两次 Err(...) 包裹,啰嗦得多。

Warning

? 的自动 From 转换是单向的、编译期确定的——它只会转成你函数声明的那个错误类型。如果你的目标类型没法 From 源错误,编译会直接报错,这时需要手动 map_err 转换。

1-8 ? 在 Option 上的应用与类型转换

? 不仅能用于 Result,也能用于 Option。当你的函数返回 Option<T> 时,内部每一步”可能为空”的查找都可以用 ? 短路:一旦某个环节是 None,整个函数立刻返回 None,省去层层 match

fn deep_lookup(map: &std::collections::HashMap<String, String>, k: &str) -> Option<String> {
    let inner = map.get(k)?;     // 没有就 None
    let val = inner.parse::<i32>().ok()?; // 解析失败转 None
    Some(format!("值={val}"))
}

注意 parse 返回的是 Result,要先 .ok() 转成 Option 才能用 ? 配合 Option 返回类型。反过来,在返回 Result 的函数里,如果想把”没找到”这种 Option 变成错误,用 ok_or(错误)None 换成 Err,再用 ? 传播。

Tip

记住这条转换链:OptionResult 之间用 ok() / ok_or(e) 互转,转完就能套上 ?。这让”查找可能空”和”操作可能错”两种流程能无缝接在同一条 ? 链上。

1-9 把 ? 串成自己的错误链

当你熟悉了 ?,会发现它能把”一连串可能失败的步骤”压成一条清爽的竖线。每一步失败就自动沿 ? 退回到函数末尾返回,你完全不必写一堆 match 去搬运错误。

use std::fs;
use std::num::ParseIntError;

fn pipeline(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let raw = fs::read_to_string(path)?;       // 读文件可能失败
    let n: i32 = raw.trim().parse()?;          // 解析可能失败
    if n <= 0 {                                // 业务校验
        return Err("数值必须为正".into());
    }
    Ok(n * 10)
}

这里 read_to_stringio::ErrorparseParseIntError 都通过 From 自动转成了 Box<dyn Error>,所以同一个返回类型能一路 ? 到底;最后那个业务错误用 Err(...) 直接返回。整个函数读起来就是”做 A,做 B,校验,成功”,失败处理被 ? 悄悄接管了。

Note

? 的心智模型特别简单:它等于”如果出错,立刻从这个函数返回那个错误”。把这个记牢,再长的错误链也不会乱。

1-10 ? 不能用来“跳过”循环

有一个常见误会要澄清:? 是”从整个函数返回”,不是”跳过本次循环”。所以你想在 for 里遇到错误就 continue 是行不通的——? 会直接结束函数。

如果要在循环里”跳过失败的、收集成功的”,用 matchif let 配合 continue;只有当你确实想”任一失败就终止整个函数”,才在循环体里用 ?。把这两个意图分清,就不会写出”本想跳过却直接退出了”的 bug。

Tip

一句话记忆:? 退的是函数,不是循环。循环里要容错,用 if let Ok(x) = ...match

1-11 当 From 转不过去:手动 map_err

? 的自动 From 转换虽好,但有前提:你的目标错误类型必须实现了 From<源错误类型>。如果没实现,编译会报错。这时就得自己动手转换,用 map_err 把错误包成你要的类型:

use std::num::ParseIntError;

fn parse_id(s: &str) -> Result<u32, String> {
    let n: u32 = s.parse().map_err(|e: ParseIntError| {
        format!("解析失败:{e}")
    })?;
    Ok(n)
}

map_err 只动错误那一侧:成功时它什么都不做、原样放行;失败时它把原来的 Err(e) 换成你返回的 Err(新值)。这样就能把各种来源的错误统一塑造成函数声明要求的那一种,再交给 ? 传播。

Note

写自己的错误枚举时,给每种源错误手写 impl From<源错误> for 我的错误,以后就能直接 ? 全自动转换,比到处 map_err 优雅。第 45 章讲 trait 约束时还会碰到这种”为别人实现 trait”的写法。

1-12 main 也能返回 Option

? 不仅能搭配 Result,在 main 里也认 Option。如果你的程序逻辑是”没拿到东西就直接结束”,可以让 main 返回 Option<T>

fn main() -> Option<()> {
    let config = load_config()?; // 没有配置就返回 None,程序退出码非 0
    run(config);
    Some(())
}

返回 Result<(), Box<dyn Error>> 适合”会发生各类错误”的程序;返回 Option<()> 适合”要么成、要么啥都没有”这种更简单的情形。?Option 上的行为依然是:遇到 None 立刻返回 None。两种起手式按你程序的实际复杂度挑一个即可。

1-13 小结

  • ? 加在 Result 表达式后:成功取值,失败立刻 return Err
  • 可链式 a()?.b()?,代码极简;
  • ? 借助 From trait 自动转换错误类型,方便归拢不同错误;
  • 用在 Option 上,遇 None 直接返回 None
  • ? 不能直接返回正确值,必须 let v = ...?; 接住;
  • 想在 main?,把返回类型改成 Result<(), Box<dyn Error>>

下一章看 Option 类型本身,以及 Rust 如何用它对空值说”不”。