上一篇入门忘了说rust的错误处理机制,本节就专门来介绍rust的错误处理机制。

Rust在语言设计层面摒弃了许多其他语言中常见的基于异常(Exception)的错误处理方式,也避免了返回空指针或特殊值(如-1)的C风格错误码。其核心是:错误是程序正常流程的一部分,必须在类型系统中显式表达和处理。 这一哲学通过两个核心的枚举类型得以实现:Option< T >Result<T, E>

1. 错误处理的基石——Result与Option

1.1 Option:处理“值可能不存在”

Option< T >用于表示一个值可能存在,也可能不存在的情况。这在处理函数返回值、结构体字段等场景中非常普遍,有效地消除了空指针或null引用带来的风险。

Option< T >有两个变体:

  • Some(T):表示值存在,并包装在Some中。
  • None:表示值不存在。

我们可以通过match表达式来安全的处理这两种情况。不过呢,Option类型还提供了一系列便利的方法来简化代码可以参考一下,例如unwrap()(如果为None则panic)、unwrap_or(default_value)、map()(对Some中的值应用函数)和and_then()(链式调用返回Option的函数)。

// 示例:查找字符串中的第一个单词
fn first_word(s: &str) -> Option<&str> {
    s.split_whitespace().next()
}

fn main() {
    let text = "hello world";
    let empty_text = "";

    // 使用match处理
    match first_word(text) {
        Some(word) => println!("第一个单词是: {}", word),
        None => println!("字符串为空或没有单词。"),
    }

    // 使用map和unwrap_or_else组合
    let word_length = first_word(empty_text)
        .map(|s| s.len()) // 如果是Some,则计算长度,返回Some(usize)
        .unwrap_or(0); // 如果是None,则返回默认值0
    
    println!("空字符串的第一个单词长度是: {}", word_length);
}

1.2 Result<T, E>:处理“操作可能失败”

当一个操作不仅可能没有值,还可能因为某种原因失败时,Result<T, E>是更合适的选择。它明确的将成功和失败两种结果编码到类型中。

Result<T, E>的两个变体:

  • Ok(T):表示操作成功,并包含成功的值。
  • Err(E):表示操作失败,并包含一个描述错误的E类型的值。

Result是处理I/O操作、网络请求、数据解析等所有可能出错场景的标准方式。与Option类似,它也拥有一套丰富的方法,如unwrap()、expect(“错误信息”)、map()、map_err()(对错误进行转换)和and_then()。

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

// 示例:读取文件内容
fn read_file_contents(path: &str) -> Result<String, std::io::Error> {
    // File::open 返回 Result<File, io::Error>
    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(e), // 如果打开失败,提前返回错误
    };

    let mut contents = String::new();
    // file.read_to_string 返回 Result<usize, io::Error>
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents), // 成功读取,返回Ok(String)
        Err(e) => Err(e),      // 读取失败,返回错误
    }
}

如上例所示,手动使用match处理Result会显得非常麻烦。这样的话我们可以引出了Rust中一个极其重要的语法糖——?操作符。

2. 简化传播——?操作符

2.1 ?操作符的核心功能

?操作符可以被附加到一个返回Result或Option类型的表达式之后。它的行为如下:

  • 作用于Result<T, E>时

    • 如果表达式的值是Ok(T),?会“解包”这个值,得到内部的T,并让程序继续执行。
    • 如果表达式的值是Err(E),?会立即中断当前函数的执行,并将这个Err(E)作为当前函数的返回值。这被称为“提前返回”(Early Return)。
  • 作用于Option< T >时

    • 如果表达式的值是Some(T),?会解包得到T。
    • 如果表达式的值是None,?会立即从当前函数返回None。

    (重要前提:?操作符只能在返回类型为Result或Option(或与之兼容的类型)的函数中使用。如果在一个返回()的main函数中使用,编译器会报错。)

使用?我们可以极大地重构上一节的代码:

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

// 使用?重构读取文件内容
fn read_file_contents_concise(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?; // 如果失败,io::Error会在这里被返回
    let mut contents = String::new();
    file.read_to_string(&mut contents)?; // 如果失败,io::Error会在这里被返回
    Ok(contents) // 所有操作都成功,返回Ok(String)
}

2.2 ?操作符与错误类型转换

?操作符最强大的功能之一是它能够与From trait集成,实现错误的自动类型转换。当?导致函数提前返回一个Err(E)时,它实际上执行的是return Err(E.into());。这意味着,只要当前函数的错误类型ErrorOuter实现了From< ErrorInner > trait,?就可以将在内部调用中产生的ErrorInner类型的错误自动转换为ErrorOuter类型。

这个机制是构建分层、可组合的错误处理系统的关键。例如,一个高级别的数据库操作函数,其错误类型可以封装来自底层网络I/O和数据解析的多种不同错误。
如下演示:

// 这是一个简化的演示
use std::io;
use std::num::ParseIntError;

// 定义一个我们自己的高级错误类型
#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(ParseIntError),
}

// 为MyError实现From<io::Error>,让?可以自动转换
impl From<io::Error> for MyError {
    fn from(err: io::Error) -> MyError {
        MyError::Io(err)
    }
}

// 为MyError实现From<ParseIntError>
impl From<ParseIntError> for MyError {
    fn from(err: ParseIntError) -> MyError {
        MyError::Parse(err)
    }
}

// 这个函数可能产生两种错误
fn get_data_from_file_and_parse(path: &str) -> Result<i32, MyError> {
    let content = read_file_contents_concise(path)?; // 返回Result<String, io::Error>
                                                     // ? 在这里如果遇到io::Error,会调用.into()将其转为MyError::Io
    
    let number = content.trim().parse::<i32>()?; // 返回Result<i32, ParseIntError>
                                                 // ? 在这里如果遇到ParseIntError,会调用.into()将其转为MyError::Parse
    Ok(number)
}

3. 构建结构化错误——std::error::Error trait

虽然可以使用简单的String或自定义enum作为错误类型,但是Rust社区有明确的确立了std::error::Error trait作为所有错误类型的“通用语言”。所以还是依据Rust社区的做法来操作吧。

3.1 Error trait的契约

一个类型要实现Error trait,必须首先实现Debug和Display trait。

  • Debug:为开发者提供详尽的调试信息,通常通过#[derive(Debug)]自动实现。
  • Display:为终端用户提供友好的、可读的错误信息。

Error trait自身提供了两个核心方法:

  • source() -> Option<&(dyn Error + 'static)>:此方法是 错误链(Error Chaining)‍ 的核心。它返回导致当前错误的底层错误(如果有的话)。通过递归调用source(),我们可以追溯到错误的根源
  • backtrace() -> Option<&Backtrace>:从Rust 1.65版本稳定后,此方法用于获取错误发生时的 回溯(Backtrace)‍ 信息,极大地增强了调试能力。回溯的捕获需要设置环境变量RUST_BACKTRACE=1 。

当一个函数需要处理多种可能的错误,但又不想定义一个巨大的枚举来包裹它们时,可以使用Box。这是一个动态分发的trait对象,可以持有任何实现了Error trait的错误类型。

use std::fmt;
use std::error::Error;

// 手动实现一个完整的错误类型
#[derive(Debug)]
struct SuperError {
    side: SuperErrorSideKick,
}

impl fmt::Display for SuperError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SuperError 发生了一个超级错误!")
    }
}

// 实现Error trait,并提供source
impl Error for SuperError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.side)
    }
}

#[derive(Debug)]
struct SuperErrorSideKick;

impl fmt::Display for SuperErrorSideKick {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "我是底层的小错误。")
    }
}

impl Error for SuperErrorSideKick {} // 它是根源错误,没有source

fn cause_error() -> Result<(), SuperError> {
    Err(SuperError { side: SuperErrorSideKick })
}

fn main() {
    if let Err(e) = cause_error() {
        println!("错误: {}", e); // 调用Display
        if let Some(source) = e.source() {
            println!("根本原因: {}", source);
        }
    }
}

Logo

新一代开源开发者平台 GitCode,通过集成代码托管服务、代码仓库以及可信赖的开源组件库,让开发者可以在云端进行代码托管和开发。旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐