Rust快速入门(7) - 错误处理

错误是软件中不可避免的一部分,Rust提供了多种机制来处理错误情况。本章总结了Rust的错误处理方式及其最佳实践。

Rust 将错误组合成两个主要类别:可恢复错误(*recoverable*)和 不可恢复错误(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是,有可恢复错误 Result<T, E> ,和不可恢复(遇到错误时停止程序执行)错误 panic!

1. 错误分类

Rust将错误分为两大类:

  1. 不可恢复错误:表示程序中的严重问题,通常是Bug的征兆,如尝试访问数组末尾之外的位置。
  2. 可恢复错误:表示可能发生但可以合理处理的问题,如文件未找到。

与其他语言使用异常处理所有错误不同,Rust使用两种不同的机制处理这两类错误:

  • 对于不可恢复错误,使用panic!
  • 对于可恢复错误,使用Result<T, E>类型

2. 不可恢复错误与panic!

2.1 触发panic的方式

有两种方式会导致panic:

  1. 执行会导致代码panic的操作(如数组越界访问)
  2. 显式调用panic!

示例:

fn main() {
    panic!("crash and burn");
}

输出:

thread 'main' panicked at src/main.rs:2:5: crash and burn

2.2 Panic时的栈回溯

当panic发生时,Rust会打印错误信息、回溯信息,然后清理栈并退出。可以通过设置环境变量RUST_BACKTRACE=1获取更详细的回溯信息。

2.3 展开或终止

当panic发生时,Rust默认会进行展开(unwinding),即回溯栈并清理每个函数的数据。也可以选择立即终止(abort)程序,这样会让操作系统负责清理内存。

可以在Cargo.toml中配置panic行为:

[profile.release]
panic = 'abort'

3. 可恢复错误与Result

3.1 Result枚举

Result<T, E>是一个枚举,定义如下:

enum Result<T, E> {
    Ok(T),   // 操作成功,包含成功值
    Err(E),  // 操作失败,包含错误信息
}

T和E是泛型参数,分别表示成功情况下的值类型和失败情况下的错误类型。

3.2 使用match处理Result

使用match表达式处理Result的例子:

use std::fs::File;

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("无法打开文件: {:?}", error),
    };
}

3.3 匹配不同的错误

可以根据错误的类型采取不同的处理方式:

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

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

    let greeting_file = match greeting_file_result {
        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);
            }
        },
    };
}

3.4 处理Result的简写方法

3.4.1 unwrap和expect

unwrap是一个快捷方法,当Result为Ok时返回值,为Err时调用panic!:

let greeting_file = File::open("hello.txt").unwrap();

expect类似于unwrap,但允许指定panic错误信息:

let greeting_file = File::open("hello.txt")
    .expect("hello.txt应该包含在此项目中");

3.4.2 传播错误

如果不想在函数中处理错误,可以将错误传播给调用者:

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

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

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

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

3.5 ?运算符简化错误传播

?运算符简化了错误传播的代码:

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

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

可以进一步简化,链式调用:

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

标准库的更简洁方法:

fn read_username_from_file() -> Result<String, io::Error> {
    std::fs::read_to_string("hello.txt")
}

3.5.1 ?运算符的限制

?运算符只能用于返回类型与?作用对象兼容的函数中。主要有两种情况:

  1. 函数返回Result<T, E>,可以对Result使用?
  2. 函数返回Option<T>,可以对Option使用?

不能在返回类型为()的函数(如main)中使用?,除非修改其返回类型:

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

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

4. 何时使用panic!与Result

4.1 决策指南

  • 当代码可能进入错误状态时使用panic!,如:
    • 遇到意外情况(而非偶尔会发生的错误)
    • 后续代码依赖于不处于错误状态的前提
    • 没有好方法用类型系统编码这些信息
  • 当错误是预期可能发生的情况时使用Result,如:
    • 解析器接收到格式错误的数据
    • HTTP请求返回表示达到速率限制的状态

4.2 适合使用panic!的场景

  • 示例代码、原型和测试
  • 你比编译器拥有更多信息,确定某些操作不会失败
  • 用户输入无效且可能导致安全问题
  • 调用你无法控制的外部代码返回无效状态

4.3 适合使用Result的场景

  • API可能合理地失败
  • 调用者需要决定如何处理失败
  • 涉及可预期的外部因素(如用户输入、网络请求等)

4.4 使用类型系统进行验证

可以利用Rust的类型系统进行验证,创建自定义类型来确保值的有效性:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("猜测值必须在1到100之间,得到了 {}", value);
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}

这样,使用Guess类型的函数就可以确信它们接收到的值始终在1到100之间。

总结

Rust的错误处理功能旨在帮助你编写更健壮的代码:

  • panic!宏表示程序处于无法处理的状态,让进程停止而不是尝试继续使用无效值
  • Result枚举利用类型系统指示操作可能以可恢复的方式失败
  • 通过在适当情况下使用panic!Result,你的代码在面对不可避免的问题时会更加可靠

Rust的错误处理哲学是明确区分不同类型的错误并强制你在编译时处理它们,这使得程序更加健壮且易于维护。


好好学习,天天向上