首页 / Rust 入门教程 / 可恢复错误 Result

Rust 入门教程

可恢复错误 Result

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

RustRust 入门教程Result可恢复错误matchunwrapexpectErr

本节目标:用 Result<T, E> 表达”可能失败”的返回值,学会用 match 取值、按错误种类分流,并分清 unwrap/expect 的使用边界。

上一章讲了严重到要崩溃的 panic。但很多错误并不致命:文件没找到、网络超时、用户输入不合法——这些都能补救。Rust 用 Result<T, E> 来承载这类可恢复错误。

1-1 Result 是什么

Result 是一个枚举,定义长这样:

enum Result<T, E> {
    Ok(T),  // 成功,里面装着正确的值
    Err(E), // 失败,里面装着错误信息
}

T 是成功时的返回值类型,E 是失败时的错误类型。因为 Result 太常用,它被放进 prelude,不用手动 use 就能直接用。

为什么 Rust 偏爱用返回值表达错误,而不是像很多语言那样抛异常?关键在于”责任归属”。在异常体系里,错误可能在任意一层被悄悄吞掉,或者一路冒泡到你没料到的地方;而 Result 把”失败”写进了函数签名——任何调用者都看得见”这个函数可能失败”,并且编译器强制你要么处理、要么显式地决定不管。错误不再是隐藏的定时炸弹,而是摆在桌面上的数据。这也正是 Rust 那句”编译器逼你考虑错误”的由来。

最典型的例子是打开文件:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}

File::open 返回 Result<std::fs::File, std::io::Error>:成功就给你一个文件句柄 Ok(File),失败就给一个 IO 错误 Err(io::Error)(比如文件不存在、没权限)。

Tip

想知道一个函数返回什么类型?装了 rust-analyzer 的 VSCode 会把类型直接标在代码上;或者故意写错类型让编译器告诉你——它会把真实类型甩在你脸上。

1-2 用 match 取出结果

拿到 Result 不处理,编译器会警告。最基础的处理是用 match 把两种情况都覆盖:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("打开文件出错: {:?}", error)
        }
    };
}

成功就把 Ok 里的句柄拿出来用;失败就 panic。这写法能跑,但把所有错误都一棍子打死,太粗暴。

1-3 按错误种类分流

真实 IO 错误有很多种,我们往往只想对”文件不存在”做特殊处理(比如新建一个),其余错误再 panic。这时嵌套 match

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("创建文件出错: {:?}", e),
            },
            other_error => panic!("打开文件出错: {:?}", other_error),
        },
    };
}

这里 error.kind() 能拿到错误的具体种类,NotFound 表示文件不存在,于是尝试 File::create 新建——而它自己又返回 Result,所以再 match 一次。逻辑清楚,但确实啰嗦,后面用 ? 能大幅精简。

1-4 unwrap 和 expect:失败就 panic

写原型、示例、测试时,你不想要 match 那么正式。这时 unwrapexpect 最顺手:成功就取出值,失败就直接 panic。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

文件不存在时,unwrap 直接崩,报错里带着具体的 Err 内容。expectunwrap 几乎一样,只是让你自定义报错信息:

let f = File::open("hello.txt").expect("打开 hello.txt 失败");

报错会变成 Failed to open hello.txt: ...,比 unwrap 默认那串更易懂。

Warning

unwrap/expect 本质是”失败即崩溃”。只用在原型、测试,或者你确信一定成功的场合(比如解析写死的 "127.0.0.1")。一旦值来自用户输入或外部系统,正式代码必须走真正的错误处理,否则程序会频繁崩。

1-5 把错误传上去:传播错误

实际项目里,函数往往嵌套十几层。错误通常在出错的那层不处理,而是往上抛,交给上游决定怎么办。这种”层层上传”叫传播错误

match 手动传播是这样:

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e), // 失败,直接把错误返回给调用者
    };

    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

函数签名 -> Result<String, io::Error> 表明:成功返回用户名 Ok(String),失败返回 io::Error。中途任何一步出错,都用 return Err(e) 把错误往上传,自己不处理。调用者拿到 Result 后再决定是继续传、panic,还是提示用户。

这种写法正确但很长。下一章的 ? 运算符就是专门来给它瘦身的。

1-6 自定义错误类型:把错误讲清楚

当项目变大,光用 Result<T, &'static str> 不够用了——字符串错误信息无法被程序区分种类。更地道的做法是定义自己的错误枚举,并为它实现标准库的 Error trait(同时需要 DebugDisplay)。

use std::fmt;

#[derive(Debug)]
enum MyErr {
    NotFound,
    BadFormat(String),
}

impl fmt::Display for MyErr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MyErr::NotFound => write!(f, "找不到资源"),
            MyErr::BadFormat(s) => write!(f, "格式错误:{s}"),
        }
    }
}

impl std::error::Error for MyErr {}

有了它,函数就能写 -> Result<u32, MyErr>,调用方用 match 精确分流。如果你不想为每个小项目都手写全套,可以临时用 Box<dyn std::error::Error> 当”万能错误”返回类型,借 ?From 自动转换,省事又兼容。

Note

标准库 trait std::error::Error 本身方法都有默认实现,你只要满足 Debug + Display 就能 impl,这是写库时的最低成本方案。

1-7 让 main 也返回 Result

前面说”函数返回 Result 才能用 ?”,那 main 函数能不能也返回 Result?答案是能。main 的签名可以写成:

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

fn main() -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string("config.txt")?;
    println!("读到 {} 字节", content.len());
    Ok(())
}

这样 main 内部就能顺畅使用 ? 传播错误;一旦某步出错,错误会沿 ? 一路回到 main,程序以非零状态码退出,并打印出错误。返回类型里的 Box<dyn Error> 是”任意实现了 Error 的错误”的万能盒子,配合 From 自动转换,几乎能接住你遇到的所有标准库错误。

Tip

小工具、命令行程序用 fn main() -> Result<(), Box<dyn Error>> 是最省心的起手式:既不用满屏 unwrap,又能把所有错误干净地交到底层。

1-8 怎么选错误处理手段

学到这里,你手里有好几把”错误处理”的钥匙,关键是什么时候用哪把。写示例、写练习、写”反正崩了也无所谓”的一次性脚本时,unwrap / expect 最快,但生产代码里要克制,因为它们把”崩溃决定权”从调用方手里抢走了。

当错误是可以预期、调用方应当处理时,用 Result 把错误交还,配合 ? 一路传播到该处理的地方。写库时,给自己的错误定义成枚举并实现 Error,或者至少在 main 层用 Box<dyn Error> 兜底,让错误信息既清晰又能被程序区分。

panic! 则留给”内部不变量被破坏、程序继续下去也没有意义”的场景——比如一个按理说绝不会为空、却为空了的缓存。一句话总结:示例图省事用 unwrap,可恢复用 Result+?,库用自定义错误,panic! 只守底线。

Note

没有”唯一正确”的选法,只有”这个错误该由谁负责”的判断。想清楚责任在谁,工具自然就选对了。

1-9 错误让函数变得可测试

Result 而不是 panic! 还有一个隐藏收益:函数变得极易测试。因为错误是”返回值”的一部分,你能在测试里断言它到底成功没、失败了又是哪一类,而不必真的让测试进程崩掉。

fn parse_age(s: &str) -> Result<u32, String> {
    let n: i32 = s.parse().map_err(|_| "不是数字".to_string())?;
    if n < 0 { return Err("年龄不能为负".into()); }
    Ok(n as u32)
}

#[test]
fn test_parse_age() {
    assert_eq!(parse_age("18"), Ok(18));
    assert!(parse_age("-1").is_err());
    assert!(parse_age("abc").is_err());
}

注意测试里没有用 unwrap 去”假设成功”,而是用 assert_eq! / is_err() 去”核对结果”。这正是 Result 的设计初衷:把”成功值”和”失败原因”都变成可以比较、可以断言的数据,而不是让程序在测试里炸开。

Tip

写业务函数时,凡是”可能失败”的,优先返回 Result;这样单测能覆盖成功路径和每一条失败路径,代码质量更有底。

1-10 取值的便捷方法

如果场景简单,不值得写一整个 matchResult 自带一批便捷方法能省不少事:

let r: Result<i32, &str> = Ok(10);

println!("{}", r.is_ok());   // true:是否成功
println!("{}", r.is_err());  // false:是否失败

let v = r.unwrap_or(0);            // 成功取值,失败给默认值 0
let v2 = r.unwrap_or_else(|e| {    // 失败时用闭包算默认值
    println!("出错:{e}");
    0
});

unwrap_or(默认) 在失败时返回一个你给的兜底值;unwrap_or_else(闭包) 更灵活,只有真的失败时才执行闭包去算默认值(适合默认值算起来比较贵的场景);还有 unwrap_or_default(),用类型的 Default 实现当兜底(比如 i320String 给空串)。这些方法在你”只关心成功值、失败给个合理默认”时非常顺手,不用写 match

Note

这些方法不会 panic,适合在”失败也无所谓、给个默认值就行”的业务分支里用。但若是文件丢失、配置缺失这类你必须知道的错误,还是老老实实用 match? 显式处理。

1-11 用 if let 只关心一边

有时候你只想处理成功、或者只处理失败,另一边懒得写。用 if let 最干净:

let r: Result<i32, &str> = Ok(10);

if let Ok(v) = r {
    println!("拿到了值:{v}"); // 只在成功时执行
}

if let Err(e) = r {
    println!("出错了:{e}");   // 只在失败时执行
}

if let Ok(v) = r 只匹配成功分支,匹配不上就跳过,省去了写完整 matchErr 处理。配合 while let 还能在循环里不断取出 Ok 值直到遇到 Err。不过要提醒一句:只写 if let Ok 而忽略 Err,等于悄悄丢掉了错误信息——写原型、调试时方便,真要上线还得把失败分支补上。

Tip

快速调试时用 if let Ok(v) 偷看一眼成功值很方便;但正式逻辑里,漏处理 Err 早晚会还债,记得把失败情况也交代清楚。

1-12 小结

  • Result<T, E>Ok(T) 表成功、Err(E) 表失败,是表达可恢复错误的标准方式;
  • match 必须同时处理 OkErr
  • 可嵌套 matcherror.kind() 细分错误种类;
  • unwrap/expect 是”失败即 panic”的快捷方式,限原型/测试/确信正确时用;
  • 不想就地处理,就 return Err(e) 把错误传播给上层。

下一章学 ? 运算符,几行就能替代上面那一长串错误传播代码。