Rust: Rust 错误处理:Result 与 ? 运算符

最后更新:2026-08-26

错误处理(Error Handling)是 Rust 最引人注目的设计之一——它不靠异常(exception)机制,而是用类型系统把"可能失败"编码进返回值类型中,编译器确保你不会忽略任何错误。

Rust 没有 try-catch,没有 throw。取而代之的是 Result<T, E> 枚举和 ? 运算符——它们让错误处理和正常业务逻辑分离开来,代码既安全又简洁。


1. 点外卖到取餐的异常处理故事

(1) 现实中的外卖流程

小明 (Xiao Ming) 今天加班,决定点一份外卖。整个流程中有多个可能出错的地方:

步骤 正常流程 可能出错
1. 找店铺 打开外卖 App,搜索店铺 找不到店铺
2. 加载菜单 浏览菜品 菜单载入失败(网络超时)
3. 提交支付 付款 支付失败(余额不足)
4. 等待制作 等待 30 分钟 店铺取消订单
5. 取餐 拿到外卖 取餐时发现订单已取消

每一步都可能成功(Ok)或失败(Err)。用 Rust 的思维来建模:

RUST
// ============================================
// 用 Result 模拟点外卖的每一步
// ============================================

// 定义可能的错误
#[derive(Debug)]
enum OrderError {
    RestaurantNotFound,
    MenuLoadFailed,
    PaymentFailed(String),
    OrderCancelled,
}

// 模拟找店铺
fn find_restaurant(name: &str) -> Result<String, OrderError> {
    let available = vec!["PizzaHouse", "SushiBar", "NoodleShop"];
    if available.contains(&name) {
        Ok(format!("Found: {}", name))
    } else {
        Err(OrderError::RestaurantNotFound)
    }
}

// 模拟加载菜单
fn load_menu(restaurant: &str) -> Result<Vec<&str>, OrderError> {
    if restaurant.contains("Pizza") {
        Ok(vec!["Margherita", "Pepperoni", "Hawaiian"])
    } else {
        Err(OrderError::MenuLoadFailed)
    }
}

// 模拟支付
fn process_payment(amount: f64) -> Result<String, OrderError> {
    if amount < 100.0 {
        Ok(format!("Paid: ${:.2}", amount))
    } else {
        Err(OrderError::PaymentFailed("Insufficient balance".into()))
    }
}

fn main() {
    // 从头到尾跑一遍完整流程
    let restaurant = find_restaurant("PizzaHouse");
    match restaurant {
        Ok(msg) => println!("Step 1: {}", msg),
        Err(e) => println!("Step 1 failed: {:?}", e),
    }
}

每一步返回 Result<T, E>——Ok(T) 表示成功,Err(E) 表示失败。调用者必须显式处理两种可能性,不可能"忘记处理错误"。


2. 概念图解

以下 Mermaid 流程图展示 Result<T, E> 错误处理的四种主要路径:match 精细处理、? 运算符传播、unwrap/expect 快速取值、以及 panic 崩溃:

100%
graph TB
    A["Result&lt;T, E&gt;"] --> B["Ok(T)<br/>成功"]
    A --> C["Err(E)<br/>失败"]

    B --> D["继续执行<br/>正常流程"]

    C --> E["match 处理<br/>精细分支控制"]
    C --> F["? 运算符<br/>向上传播错误"]
    C --> G["unwrap / expect<br/>快速取值"]

    E --> H["对不同错误<br/>分别处理"]
    F --> I["调用者函数<br/>接收 Err"]
    G --> J["panic!<br/>程序崩溃"]

    H --> K["返回默认值<br/>或重试逻辑"]
    I --> L["main 中 match<br/>统一处理"]

    style A fill:#e1f5fe,stroke:#0288d1
    style B fill:#c8e6c9,stroke:#388e3c
    style C fill:#ffcdd2,stroke:#d32f2f
    style J fill:#ffcdd2,stroke:#d32f2f

3. 你将学到


4. 核心概念

100%
graph TB
    A[错误处理策略] --> B[可恢复错误<br>Recoverable]
    A --> C[不可恢复错误<br>Unrecoverable]

    B --> D["Result<T, E>"]
    D --> E["Ok(T) 成功值"]
    D --> F["Err(E) 错误值"]

    F --> G["match 精细处理"]
    F --> H["? 向上传播"]
    F --> I["unwrap/expect 快速取值<br>(有风险)"]

    C --> J["panic!"]
    J --> K["程序崩溃退出"]
    J --> L["适用:Bug/不可恢复状态"]

    B -.-> M["自定义错误类型"]
    M --> N["实现 Display + Debug"]
    M --> O["From trait 转换"]

(1) 四种错误处理策略对比

策略 使用场景 优点 缺点
panic! 不可恢复错误(如数组越界、断言失败) 快速失败,暴露问题 程序直接崩溃
unwrap / expect 原型开发/确定不会失败 代码简洁 出错时直接 panic,不优雅
match / if let 需要对不同错误做不同处理 精细控制错误处理逻辑 代码冗长
? 运算符 函数间传播错误,上层统一处理 最简洁,保持主逻辑清晰 需要在返回 Result 的函数中使用

(2) Result<T, E> 方法速查

方法 签名 作用 失败时行为
unwrap() Result<T,E> -> T 取出 Ok 中的值 panic!
expect(msg) Result<T,E> -> T 取出 Ok 中的值,自定义 panic 信息 panic!(msg)
unwrap_or(default) Result<T,E> -> T 成功返回值,失败返回默认值 返回默认值
unwrap_or_else(fn) Result<T,E> -> T 成功返回值,失败执行闭包 执行闭包
is_ok() Result<T,E> -> bool 判断是否成功
is_err() Result<T,E> -> bool 判断是否失败
ok() Result<T,E> -> Option<T> 转为 Option None
err() Result<T,E> -> Option<E> 转为 Option None
map(fn) Result<T,E> -> Result<U,E> 对成功值做转换 不变
map_err(fn) Result<T,E> -> Result<T,F> 对错误值做转换 转换错误类型
and_then(fn) Result<T,E> -> Result<U,E> 链式调用后续操作 短路

5. 示例

▶ 示例 1:基础 Result 与 match 处理(难度 ⭐)

RUST
// ============================================
// 用 Result 处理除法运算中的除零错误
// ============================================

fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    // 用 match 处理成功和失败两种情况
    println!("=== match 处理 ===");
    match safe_divide(10.0, 2.0) {
        Ok(result) => println!("10 / 2 = {}", result),
        Err(msg) => println!("Error: {}", msg),
    }

    match safe_divide(10.0, 0.0) {
        Ok(result) => println!("10 / 0 = {}", result),
        Err(msg) => println!("Error: {}", msg),
    }

    // 用 unwrap_or 提供默认值
    println!("\n=== unwrap_or 默认值 ===");
    let result1 = safe_divide(10.0, 2.0).unwrap_or(0.0);
    let result2 = safe_divide(10.0, 0.0).unwrap_or(0.0);
    println!("10 / 2 = {}", result1);
    println!("10 / 0 = {} (default)", result2);

    // 用 unwrap_or_else 执行闭包
    println!("\n=== unwrap_or_else 闭包 ===");
    let result3 = safe_divide(10.0, 2.0).unwrap_or_else(|e| {
        eprintln!("Warning: {}, using default", e);
        0.0
    });
    println!("Result: {}", result3);

    // ⚠️ unwrap 会 panic(取消注释下面代码来体验)
    // let crash = safe_divide(10.0, 0.0).unwrap();
    // println!("Will not reach here");
}

输出:

TEXT 📖 仅展示
=== match 处理 ===
10 / 2 = 5
Error: Division by zero

=== unwrap_or 默认值 ===
10 / 2 = 5
10 / 0 = 0 (default)

=== unwrap_or_else 闭包 ===
Result: 5

match 处理最完整——你可以针对 OkErr 分别写处理逻辑。unwrap_orunwrap_or_else 是快捷方式:失败时提供默认值。unwrap() 最危险——它假设一定成功,失败就直接崩溃。


▶ 示例 2:? 运算符——链式传播错误(难度 ⭐⭐)

RUST
// ============================================
// ? 运算符:在 Result 间传播错误
// 只有在返回 Result 的函数中才能使用 ?
// ============================================

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

// 读取文件的完整内容
// ? 表示:如果 File::open 失败,立即返回 Err
//         如果 read_to_string 失败,立即返回 Err
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;          // 打开文件,失败就返回
    let mut content = String::new();
    file.read_to_string(&mut content)?;         // 读取内容,失败就返回
    Ok(content)
}

// 不使用 ? 的等价写法——代码量翻倍
fn read_file_without_question(path: &str) -> Result<String, io::Error> {
    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut content = String::new();
    match file.read_to_string(&mut content) {
        Ok(_) => Ok(content),
        Err(e) => Err(e),
    }
}

// 链式调用 ?——更简洁
fn read_file_chain(path: &str) -> Result<String, io::Error> {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    // 分别测试读得到和读不到的文件
    let files = vec!["Cargo.toml", "nonexistent.txt"];

    for file in &files {
        match read_file(file) {
            Ok(content) => {
                println!("--- {} ({} bytes) ---", file, content.len());
                println!("{}", &content[..content.len().min(80)]);
            }
            Err(e) => {
                println!("Failed to read '{}': {}", file, e);
            }
        }
    }
}

输出(假设 Cargo.toml 存在且不为空):

TEXT 📖 仅展示
--- Cargo.toml (42 bytes) ---
[package]
name = "demo"
version = "0.1.0"
edition = "2021"

Failed to read 'nonexistent.txt': The system cannot find the file specified. (os error 2)

? 运算符是 Rust 错误处理的精髓:它相当于一个"失败就提前返回"的简写。expr? 等价于 match expr { Ok(v) => v, Err(e) => return Err(e.into()) }。注意 ? 会自动调用 From::from 做错误类型转换——这是它可以跨不同错误类型传播的关键。


▶ 示例 3:自定义错误类型——实现 Display + Debug(难度 ⭐⭐)

RUST
// ============================================
// 自定义错误类型:让你的错误信息更丰富
// 需要实现 std::fmt::Display + std::fmt::Debug
// ============================================

use std::fmt;
use std::num::ParseIntError;

// 自定义错误枚举
#[derive(Debug)]
enum AppError {
    /// 输入为空
    EmptyInput,
    /// 解析数字失败,附带原始字符串
    ParseFailed(String),
    /// 数值超出允许范围
    OutOfRange { value: i32, min: i32, max: i32 },
    /// 除以零
    DivisionByZero,
}

// 实现 Display——控制用户看到的错误信息
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::EmptyInput => {
                write!(f, "Input cannot be empty")
            }
            AppError::ParseFailed(input) => {
                write!(f, "Failed to parse '{}' as a number", input)
            }
            AppError::OutOfRange { value, min, max } => {
                write!(f, "Value {} is out of range [{}, {}]", value, min, max)
            }
            AppError::DivisionByZero => {
                write!(f, "Division by zero is not allowed")
            }
        }
    }
}

// 实现 From<ParseIntError>——让 ? 运算符能自动转换
impl From<ParseIntError> for AppError {
    fn from(_: ParseIntError) -> Self {
        AppError::ParseFailed("unknown".into())
    }
}

// 处理用户输入:解析并验证
fn process_input(input: &str, divisor: i32) -> Result<i32, AppError> {
    if input.is_empty() {
        return Err(AppError::EmptyInput);
    }
    let value: i32 = input.parse().map_err(|_| {
        // 手动转换错误类型
        AppError::ParseFailed(input.to_string())
    })?;
    if value < -100 || value > 100 {
        return Err(AppError::OutOfRange {
            value,
            min: -100,
            max: 100,
        });
    }
    if divisor == 0 {
        return Err(AppError::DivisionByZero);
    }
    Ok(value / divisor)
}

fn main() {
    let test_cases = vec![
        ("42", 2,    "Normal case"),
        ("",   1,    "Empty input"),
        ("abc", 1,   "Parse error"),
        ("999", 1,   "Out of range"),
        ("50",  0,   "Division by zero"),
    ];

    for (input, divisor, description) in test_cases {
        match process_input(input, divisor) {
            Ok(result) => println!("[{}] OK: {}", description, result),
            Err(e) => println!("[{}] Error: {}", description, e),
        }
    }
}

输出:

TEXT 📖 仅展示
[Normal case] OK: 21
[Empty input] Error: Input cannot be empty
[Parse error] Error: Failed to parse 'abc' as a number
[Out of range] Error: Value 999 is out of range [-100, 100]
[Division by zero] Error: Division by zero is not allowed

自定义错误类型需要实现 Display(用户看到的错误信息)和 Debug{:?} 调试输出)。实现 From<T> trait 可以让 ? 运算符自动将某种错误类型转换为你自定义的类型——这是 ? 能够跨类型传播的核心机制。


▶ 示例 4:panic! vs 错误返回值的选择策略(难度 ⭐⭐⭐)

RUST
// ============================================
// 演示 panic! 和错误返回值的适用场景
// panic! → 不可恢复错误(Bug/断言失败)
// Result → 可恢复错误(用户输入/IO失败)
// ============================================

// --- 适合作 panic! 的场景 ---

/// 从配置文件中读取端口号
/// 如果配置文件缺失,这属于程序 Bug,panic 是合理的
fn get_default_port() -> u16 {
    // 这个值硬编码在代码中,不可能会解析失败
    "8080"
        .parse()
        .expect("Hardcoded port number is invalid")
}

/// 只接受正整数的函数
/// 传入负数说明调用方有 Bug,panic 快速暴露问题
fn sqrt_unchecked(x: i32) -> f64 {
    if x < 0 {
        panic!("sqrt_unchecked called with negative value: {}", x);
    }
    (x as f64).sqrt()
}

// --- 适合作 Result 的场景 ---

/// 从用户输入解析数字
/// 用户输入错误的格式是正常的,应当返回 Result
fn parse_user_input(input: &str) -> Result<i32, String> {
    input
        .parse()
        .map_err(|_| format!("'{}' is not a valid integer", input))
}

/// 计算身体质量指数(BMI)
/// 体重为 0 或负数是可能的数据错误,不是程序 Bug
fn calculate_bmi(weight_kg: f64, height_m: f64) -> Result<f64, String> {
    if weight_kg <= 0.0 {
        return Err("Weight must be positive".to_string());
    }
    if height_m <= 0.0 {
        return Err("Height must be positive".to_string());
    }
    Ok(weight_kg / (height_m * height_m))
}

fn main() {
    // 场景 1:panic 用于不可恢复错误
    println!("Default port: {}", get_default_port());

    // 场景 2:panic 用于断言——参数不合法
    let value = 16;
    println!("sqrt({}) = {}", value, sqrt_unchecked(value));

    // 场景 3:Result 用于可恢复错误——用户输入
    let inputs = vec!["42", "hello", "-5"];
    for input in inputs {
        match parse_user_input(input) {
            Ok(n) => println!("Parsed: {}", n),
            Err(e) => println!("Parse failed: {}", e),
        }
    }

    // 场景 4:Result 用于业务逻辑验证
    let bmi_cases = vec![
        (70.0, 1.75),
        (0.0, 1.70),
        (65.0, -0.5),
    ];
    for (weight, height) in bmi_cases {
        match calculate_bmi(weight, height) {
            Ok(bmi) => println!("BMI: {:.1}", bmi),
            Err(e) => println!("BMI error: {}", e),
        }
    }

    // 场景 5:expect 的 panic 信息帮助调试
    let numbers = vec![10, 20, 30];
    let first = numbers.first().expect("Vector should not be empty");
    println!("First element: {}", first);
}

输出:

TEXT 📖 仅展示
Default port: 8080
sqrt(16) = 4
Parsed: 42
Parse failed: 'hello' is not a valid integer
Parsed: -5
BMI: 22.9
BMI error: Weight must be positive
BMI error: Height must be positive
First element: 10

选择策略的核心原则panic! 用于"程序自己的 Bug"(硬编码数据无效、传入违反约定的参数、数组越界);Result 用于"外部环境或用户输入导致的异常"(IO 错误、解析失败、业务验证不通过)。通俗讲:你能修复的用 Result,你修不了的用 panic!


6. 关于 anyhowthiserror 的概念介绍

在生产级 Rust 项目中,有两个社区 crate 被广泛使用来简化错误处理:

Crate 主要用途 适用场景 核心特性
anyhow 错误传播(调用方视角) 应用程序 main 函数、CLI 工具、脚本 anyhow::Result<T>.context() 给错误加上下文
thiserror 错误定义(库作者视角) 库的公共 API 错误类型 用 derive 宏自动生成 Display + Error 实现
RUST
// anyhow 风格(概念示例,不要求运行)
// use anyhow::{Context, Result};
//
// fn read_config() -> Result<String> {
//     let content = std::fs::read_to_string("config.toml")
//         .context("Failed to read config file")?;
//     Ok(content)
// }

// thiserror 风格(概念示例,不要求运行)
// use thiserror::Error;
//
// #[derive(Error, Debug)]
// enum MyError {
//     #[error("IO error: {0}")]
//     Io(#[from] std::io::Error),
//
//     #[error("Parse error: {0}")]
//     Parse(#[from] std::num::ParseIntError),
// }

anyhow 让你关注"怎么处理错误"而非"怎么定义错误";thiserror 让你用注解代替手写 Display + From 实现。两者通常配合使用:库用 thiserror 定义细粒度错误类型,应用用 anyhow 统一传播。


▶ 示例 5:综合练习——多层错误传播与恢复(难度 ⭐⭐⭐)

RUST
// ============================================
// 综合示例:自定义错误 + ? 传播 + 恢复策略
// ============================================

use std::fmt;

#[derive(Debug)]
enum AppError {
    ParseError(String),
    ValidationError(String),
    NotFound(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::ParseError(msg) => write!(f, "解析错误: {}", msg),
            AppError::ValidationError(msg) => write!(f, "验证错误: {}", msg),
            AppError::NotFound(msg) => write!(f, "未找到: {}", msg),
        }
    }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self {
        AppError::ParseError(e.to_string())
    }
}

fn parse_age(input: &str) -> Result<u8, AppError> {
    let age: u8 = input.parse().map_err(|_| AppError::ParseError(format!("'{}' 不是有效数字", input)))?;
    if age > 150 {
        return Err(AppError::ValidationError(format!("年龄 {} 不合理", age)));
    }
    Ok(age)
}

fn find_user(id: u32) -> Result<String, AppError> {
    let users = [(1, "Alice"), (2, "Bob"), (3, "Charlie")];
    users.iter()
        .find(|(uid, _)| *uid == id)
        .map(|(_, name)| name.to_string())
        .ok_or_else(|| AppError::NotFound(format!("用户 ID={}", id)))
}

fn process_user(id_str: &str, age_str: &str) -> Result<String, AppError> {
    let id: u32 = id_str.parse().map_err(|_| AppError::ParseError(format!("无效 ID: '{}'", id_str)))?;
    let age = parse_age(age_str)?;
    let name = find_user(id)?;
    Ok(format!("用户: {}, 年龄: {}", name, age))
}

fn main() {
    let test_cases = [
        ("1", "30"),
        ("2", "200"),
        ("5", "25"),
        ("abc", "30"),
        ("3", "abc"),
    ];

    println!("=== 用户处理测试 ===");
    for (id, age) in &test_cases {
        match process_user(id, age) {
            Ok(result) => println!("成功: {}", result),
            Err(e) => println!("失败: {}", e),
        }
    }

    println!("\n=== 批量处理(容错)===");
    let inputs = [("1", "30"), ("5", "25"), ("2", "abc"), ("3", "20")];
    let mut success_count = 0;
    for (id, age) in &inputs {
        match process_user(id, age) {
            Ok(result) => { println!("OK: {}", result); success_count += 1; }
            Err(e) => println!("跳过: {}", e),
        }
    }
    println!("成功: {}/{}", success_count, inputs.len());
}

输出:

TEXT 📖 仅展示
=== 用户处理测试 ===
成功: 用户: Alice, 年龄: 30
失败: 验证错误: 年龄 200 不合理
失败: 未找到: 用户 ID=5
失败: 解析错误: 无效 ID: 'abc'
失败: 解析错误: 'abc' 不是有效数字

=== 批量处理(容错)===
OK: 用户: Alice, 年龄: 30
跳过: 未找到: 用户 ID=5
跳过: 解析错误: 'abc' 不是有效数字
OK: 用户: Charlie, 年龄: 20
成功: 2/4

自定义 AppError 枚举 + From 转换 + ? 传播,让错误处理链清晰简洁。map_err 将底层错误转为自定义类型。批量处理时用 match 做容错——不中断循环,记录失败并继续。


❓ 常见问题

Q unwrap() 是不是一种不好的实践?
A 是,除非你能肯定不会出错。
Q ? 运算符和 match 处理错误,应该优先用哪个?
A 优先用 ? 传播,在需要精细处理的地方用 match
Q 我的函数里有多种错误类型(IO 错误、解析错误、业务错误),? 怎么能统一传播?
AFrom trait 自动转换。
Q panic! 能捕获吗?
A 可以,但不应作为常规错误处理方式。
Q 自定义错误类型为什么需要同时实现 DisplayDebug
A 因为 Rust 的 Error trait 要求两者兼备。

📖 小节


📝 作业

  1. 难度 ⭐:写一个函数 fn parse_age(input: &str) -> Result<u8, String>,将字符串解析为年龄(0-150)。如果解析失败或数值超出范围,返回对应的错误信息。在 main 中用 match 处理三种情况:正常年龄、非数字输入、超出范围。

  2. 难度 ⭐⭐:写一个嵌套调用的场景——函数 A 调用函数 B,函数 B 调用函数 C,每层都可能失败。使用 ? 运算符传播错误。场景:read_user_file() -> parse_user_data() -> validate_age()。每层返回同一自定义错误类型 UserDataError(用枚举定义),包含 FileNotFoundParseFailed(String)InvalidAge(i32) 三种变体。

  3. 难度 ⭐⭐⭐:设计一个迷你计算器,支持 addsubtractmultiplydivide 四种运算,所有运算通过 ? 链式传播错误。要求:

    • 定义 CalcError 枚举(DivideByZeroOverflowInvalidOperator(String)
    • 实现 Display + Debug
    • ? 串联多个操作:calculate("10 + 5 * 2") 这样的字符串表达式
    • 提示:先按空格分割字符串,然后用 fold 或循环逐个处理
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏