Android程序员初学Rust-错误处理

Rust 的错误处理机制以安全性和明确性为核心,通过 panicResult 双轨制应对不同场景。panic 处理不可恢复的致命错误,而 Result 则优雅管理可预测异常。本文将深入解析其设计哲学、? 运算符的魔法、自定义错误实践,并探讨 thiserror / anyhow 库如何简化开发,助你构建健壮的 Rust 程序。

Panic

Rust 通过 panic 机制来处理致命错误。

如果在运行时发生致命错误,Rust 将触发 panic

Rust 复制代码
fn main() {
    let v = vec![10, 20, 30];
    dbg!(v[100]);
}

panic 会直接导致程序退出,用于处理不可恢复且意外的错误。

  • panic 是程序中存在漏洞的迹象。
  • 运行时失败(如越界检查失败)可能会引发 panic
  • 断言(如 assert!)失败时会引发 panic
  • 特定目的的 panic 可以使用 panic! 宏。

panic 会展开栈,上述代码会打印如下的栈信息:

如果程序不允许崩溃,请使用不会引发 panicAPI (如 Vec::get)。

Rust 复制代码
fn main() {
    let v = vec![10, 20, 30];
    println!("{}", v.get(100).unwrap_or(&-1));
    println!("{}", v.get(1).unwrap_or(&-1));
}

默认情况下,一次 panic 会导致栈展开。栈展开过程可以被捕获:

Rust 复制代码
use std::panic;

fn main() {
    let result = panic::catch_unwind(|| "No problem here!");
    dbg!(result);

    let result = panic::catch_unwind(|| {
        panic!("oh no!");
    });
    dbg!(result);
}

捕获 panic 并不常见;不要尝试使用 catch_unwind 来实现异常处理!

这在服务器中可能很有用,即使单个请求崩溃,服务器也应继续运行。

如果在你的 Cargo.toml 文件中设置了 panic = 'abort',则捕获 panic 就不起作用了。

Result

Rust 中,我们用于错误处理的主要机制是 Result 枚举,我们在讨论标准库时曾简要提及过它。

Rust 复制代码
use std::fs::File;
use std::io::Read;

fn main() {
    let file: Result<File, std::io::Error> = File::open("diary.txt");
    match file {
        Ok(mut file) => {
            let mut contents = String::new();
            if let Ok(bytes) = file.read_to_string(&mut contents) {
                println!("Dear diary: {contents} ({bytes} bytes)");
            } else {
                println!("Could not read file content");
            }
        }
        Err(err) => {
            println!("The diary could not be opened: {err}");
        }
    }
}

Result 有两个成员:Ok,它包含成功值;Err,它包含某种类型的错误值。

一个函数是否产生错误可以通过函数返回 Result 来判断。

Option 类似,Result 不存在忘记处理错误的情况:在对 Result 进行模式匹配以检查属于哪个成员之前,你无法访问到成功值或错误值。

unwrap 这样的方法使得编写简单但不健壮的的代码变得更容易,使用 unwrap()expect() 可能隐藏错误,但这也意味着在你的源代码中,你始终能够看出在哪些地方跳过了错误处理。

一些语言使用异常,例如 C++JavaPython

在支持异常的语言中,一个函数是否会抛出异常并不会作为其类型签名的一部分显现出来。这通常意味着在调用一个函数时,你无法判断它是否可能会抛出异常。

异常通常会展开调用栈,向上传播,直到到达一个 try 块。一个源自调用栈深处的错误可能会影响到更上层一个不相关的函数。

一些语言让函数在返回成功值的同时返回一个错误码(或其他某种错误值)。比如 CGo

根据不同语言的情况,有可能会忘记检查错误值,如果这样,您可能正在访问一个未初始化或无效的成功值。

Rust 使用了另一种方案------Result,当一个函数返回 Result 时,开发者就会立即知道这个函数可能会返回一个错误,开发者无法简单的忽略 Result 直接获取正确的值,必须处理 Result。要么使用模式匹配,要么使用类似 unwrap_xxx 的函数获取正确的值。

? 操作符

诸如连接被拒绝或文件未找到之类的运行时错误使用 Result 类型进行处理,但每次调用时都匹配该类型可能会很麻烦。? 操作符用于将错误返回给调用者。它能让你把常见的情况转化为简单得多的:

Rust 复制代码
match some_expression {
    Ok(value) => value,
    Err(err) => return Err(err),
}

上述代码可以使用更加简单的形式:

Rust 复制代码
some_expression?

我们可以用这个来简化我们的错误处理代码:

Rust 复制代码
use std::io::Read;
use std::{fs, io};

fn read_username(path: &str) -> Result<String, io::Error> {
    let username_file_result = fs::File::open(path);
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(err) => return Err(err),
    };

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

简化后的代码:

Rust 复制代码
use std::io::Read;
use std::{fs, io};

fn read_username(path: &str) -> Result<String, io::Error> {
    let mut username_file = fs::File::open(path)?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

很简洁,不是吗?

实际上,? 的展开比之前所讲的要更复杂一点:

Rust 复制代码
expression?

会展开为:

Rust 复制代码
match expression {
    Ok(value) => value,
    Err(err)  => return Err(From::from(err)),
}

这里的 From::from 调用意味着我们试图将错误类型转换为函数返回的类型。这使得将错误封装到更高级别的错误中变得更容易。

例如:

Rust 复制代码
use std::error::Error;
use std::io::Read;
use std::{fmt, fs, io};

#[derive(Debug)]
enum ReadUsernameError {
    IoError(io::Error),
    EmptyUsername(String),
}

impl Error for ReadUsernameError {}

impl fmt::Display for ReadUsernameError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::IoError(e) => write!(f, "I/O error: {e}"),
            Self::EmptyUsername(path) => write!(f, "Found no username in {path}"),
        }
    }
}

impl From<io::Error> for ReadUsernameError {
    fn from(err: io::Error) -> Self {
        Self::IoError(err)
    }
}

fn read_username(path: &str) -> Result<String, ReadUsernameError> {
    let mut username = String::with_capacity(100);
    fs::File::open(path)?.read_to_string(&mut username)?; // 注意这里使用了简化的 ?. 链式调用
    if username.is_empty() {
        return Err(ReadUsernameError::EmptyUsername(String::from(path)));
    }
    Ok(username)
}

fn main() {
    //std::fs::write("config.dat", "").unwrap();
    let username = read_username("config.dat");
    println!("username or error: {username:?}");
}

? 运算符必须返回与函数返回类型兼容的值。对于 Result 来说,这意味着错误类型必须兼容。一个返回 Result<T, ErrorOuter> 的函数,只有当 ErrorOuterErrorInner 是相同类型,或者 ErrorOuter 实现了 From<ErrorInner> 时,才能对 Result<U, ErrorInner> 类型的值使用 ?

上述代码中,实现了 impl From<io::Error> for ReadUsernameError,所以可以直接使用 fs::File::open(path)?.read_to_string

实现 From 的一个常见替代方法是使用 Result::map_err,特别是当转换只在一个地方进行时。

对于 Option 没有兼容性要求。一个返回 Option<T> 的函数可以对任意 TU 类型的 Option<U> 使用 ? 运算符。

返回 Result 的函数不能对 Option 使用 ?,反之亦然。不过,Option::ok_or 可以将 Option 转换为 Result,而 Result::ok 可以将 Result 转换为 Option

Rust 复制代码
let op = Option::Some(10);
let res = op.ok_or(ParseError);

let res: Result<i32, ParseError> = Ok(10);
let op = res.ok();

动态 Error

有时候,我们希望允许返回任何类型的错误,而无需编写涵盖所有不同可能性的自定义枚举。std::error::Error trait 使得创建一个可以包含任何错误的 trait 对象变得很容易:

Rust 复制代码
use std::error::Error;
use std::fs;
use std::io::Read;

fn read_count(path: &str) -> Result<i32, Box<dyn Error>> {
    let mut count_str = String::new();
    fs::File::open(path)?.read_to_string(&mut count_str)?;
    let count: i32 = count_str.parse()?;
    Ok(count)
}

fn main() {
    fs::write("count.dat", "1i3").unwrap();
    match read_count("count.dat") {
        Ok(count) => println!("Count: {count}"),
        Err(err) => println!("Error: {err}"),
    }
}

read_count 函数可能会返回 std::io::Error(源自文件操作) 或 std::num::ParseIntError(源自 String::parse)。

将错误放入 Box 虽然能减少代码量,但会丧失在程序中对不同错误情况进行清晰处理的能力。因此,在库的公共 API 中使用 Box<dyn Error> 通常不是一个好主意,但在一个仅希望在某处显示错误消息的程序中,它可能是一个不错的选择。

在定义自定义错误类型时,请确保实现 std::error::Error trait,这样它才能被放入 Box<dyn Error>

thiserror

开源库 thiserror 提供了一些宏,有助于在定义错误类型时避免编写冗长的样板代码。它提供了一些派生宏,可帮助实现 From<T>DisplayError trait

使用 thiserror,需要在 Cargo.toml 中添加:

Toml 复制代码
[dependencies]
thiserror = "xxx"
Rust 复制代码
use std::io::Read;
use std::{fs, io};
use thiserror::Error;

#[derive(Debug, Error)]
enum ReadUsernameError {
    #[error("I/O error: {0}")]
    IoError(#[from] io::Error),
    #[error("Found no username in {0}")]
    EmptyUsername(String),
}

fn read_username(path: &str) -> Result<String, ReadUsernameError> {
    let mut username = String::with_capacity(100);
    fs::File::open(path)?.read_to_string(&mut username)?;
    if username.is_empty() {
        return Err(ReadUsernameError::EmptyUsername(String::from(path)));
    }
    Ok(username)
}

fn main() {
    //fs::write("config.dat", "").unwrap();
    match read_username("config.dat") {
        Ok(username) => println!("Username: {username}"),
        Err(err) => println!("Error: {err:?}"),
    }
}

// Output
// Error: IoError(Os { code: 2, kind: NotFound, message: "No such file or directory" })

Error 派生宏由 thiserror 提供,它有许多有用的属性,有助于以紧凑的方式定义错误类型。

#[error] 中的消息用于派生 Display trait

请注意,thiserror::Error 派生宏虽然具有实现 std::error::Error trait 的效果,但不是同一个类型;该 trait 和宏不属于同一个命名空间。

anyhow

开源库 anyhow 提供了一种功能丰富的错误类型,支持携带额外的上下文信息,可用于为程序出错前的操作提供语义化的追踪信息。

这可以与 thiserror 中的便捷宏结合使用,从而避免为自定义错误类型显式编写 trait 实现。

Rust 复制代码
use anyhow::{Context, Result, bail};
use std::fs;
use std::io::Read;
use thiserror::Error;

#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("Found no username in {0}")]
struct EmptyUsernameError(String);

fn read_username(path: &str) -> Result<String> {
    let mut username = String::with_capacity(100);
    fs::File::open(path)
        .with_context(|| format!("Failed to open {path}"))?
        .read_to_string(&mut username)
        .context("Failed to read")?;
    if username.is_empty() {
        bail!(EmptyUsernameError(path.to_string()));
    }
    Ok(username)
}

fn main() {
    //fs::write("config.dat", "").unwrap();
    match read_username("config.dat") {
        Ok(username) => println!("Username: {username}"),
        Err(err) => println!("Error: {err:?}"),
    }
}

anyhow::Error 本质上是对 Box<dyn Error> 的封装。因此,对于库的公共 API 来说,它可能不是一个好的选择,但在应用程序中被广泛使用。

anyhow::Result<V>Result<V, anyhow::Error> 的类型别名。

anyhow::Context 是为标准的 ResultOption 类型实现的一个 trait 。要在这些类型上使用 .context().with_context() ,需要导入 use anyhow::Context,该方法会使用额外的上下文包装错误值。

相关推荐
love530love2 小时前
【笔记】在 MSYS2(MINGW64)中正确安装 Rust
运维·开发语言·人工智能·windows·笔记·python·rust
景天科技苑8 小时前
【Rust宏编程】Rust有关宏编程底层原理解析与应用实战
开发语言·后端·rust·rust宏·宏编程·rust宏编程
维维酱10 小时前
Rust - 消息传递
rust
Kapaseker15 小时前
Android程序员初学Rust-线程
rust
solohoho16 小时前
Rust:所有权的理解
rust
猩猩程序员16 小时前
十年下注 Rust,我期待的下一个十年
rust
Humbunklung1 天前
Rust 控制流
开发语言·算法·rust
UestcXiye2 天前
Rust 学习笔记:Box<T>
rust
用户27692024453462 天前
基于 Tauri + Vue3 的现代化新流串口调试助手 v2
前端·rust