掘金 后端 ( ) • 2024-04-23 10:42

本教程环境

系统:MacOS

Rust 版本:1.77.2

错误在程序开发中是不可避免的。Rust 将错误分为两大类:可恢复的不可恢复的。 发生不可恢复的错误,例如数组越界,程序会 panic。 同时,Rust 提供了 Result<T, E> 类型来处理可恢复的错误,例如文件读取错误。

panic

用来处理永远不应该发生的错误。例如:

  • 数组越界访问;
  • 整数除以 0;
  • 在恰好为 Err 的 Result 上调用 .expect();
  • 断言失败。

当代码检测到出现错误需要立即触发 panic 时,可以使用 panic!() 宏。 Rust 在发生 panic 时可以展开调用栈(默认),也可中止进程。

展开调用栈

例如当整数除以 0 的时候会触发 panic。

fn main() {
    println!("{}", 100/0);
}

报错如下: 截屏2024-01-25 16.45.19.png 在 Rust 中触发了 panic,通常会按如下方式处理:

  • 打印一条错误信息到终端。
  • 展开调用栈;
  • 最后,线程退出。如果 panic 是主线程,退出整个进程。

panic 是基于线程的。一个线程 panic,其他线程可以继续做自己的事。 还有一种方式可以捕获调用栈展开,让线程存活并继续运行。std::panic::catch_unwind() 可以做到这一点。这是 Rust 的测试工具用于在测试中断言失败时进行恢复的机制。

中止

展开调用栈事默认的 panic 行为,但是,在两种情况下 Rust 不会试图展开调用栈。 Rust 在试图处理第一个 panic 时,.drop() 方法触发了第二个 panic,那么这个 panic 就是致命的。Rust 会停止展开调用栈并中止整个进程。 此外,Rust 处理 panic 的行为是可定制的。如果使用 -C panic=abort 参数进行编译,那么程序中的第一个 panic 会立即中止进程。

Result

Rust 中没有异常。函数执行失败时候会返回 Result 类型。它会指示出可能的失败。要么返回一个成功的结果,要么返回一个错误的结果。

fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>

捕获错误

可使用 match 来处理 Result

match get_weather(hometown) {
	Ok(report) => {
		display_weather(hometown, &report);
	}
	Err(err) => {
		println!("error querying the weather: {}", err);
		shedule_weather_retry();
	}
}

match 有点冗长,所以针对一些常见场景提供了多个有用方法。

  • result.is_ok() 已成功
  • result.is_err() 已出错
  • result.ok() 成功值
  • result.err() 错误值
  • result.unwarp_or(fallback) 如果 result 为成功结果,就返回成功值;否则,返回 fallback,丢弃错误值。
  • result.unwarp_or_else(fallbakc_fn) 解包,否则调用
  • result.unwarp() 解包。如果 result 错误,发生 panic。
  • result.expect(message)unwarp() 相同,但是可以提供在 panic 时的消息。
  • result.as_ref() 转引用,将 Result<T, E> 转为 Result<&T, &E>
  • result.as_mut() 转可变引用。

Result 类型别名

如果 Rust 文档中忽略了 Result 中的错误类型:

fn remove_file(path: &Path) -> Result<()>

这意味着使用了 Result 的类型别名。 例如,标准库的 std::io 模块定义了如下的别名。

pub type Result<T> = result::Result<T, Error>;

打印错误

标准库中定义了几种错误类型。

  • std::io::Error;
  • std::fmt::Error;
  • std:::str::Utf8Error

它们都实现了 std::error::Error 特型,意味着它们都有以下特性和方法:

  • 可通过 println!() 进行打印。使用格式说明符 {} 打印简短错误信息。或使用 {:?} 获取该错误的 Debug 视图。
  • err.to_string() 转字符串,以 String 形式返回错误信息。
  • err.source() 错误来源。

如果要打印一个错误的所有可用信息,可使用下面的方法:

use std::{
    error::Error,
    io::{stderr, Write},
};

fn print_error(mut err: &dyn Error) {
    let _ = writeln!(stderr(), "error: {}", err);
    while let Some(source) = err.source() {
        let _ = writeln!(stderr(), "caused by: {}", source);
        err = source;
    }
}

writeln! 宏类似 println! 它会将数据写入所选的流。上面代码将错误消息写入了标准错误流 std::io::stderr。 可使用 anyhow crate 提供一个现成的错误类型。

传播错误

希望错误暂时不处理,而是沿着调用栈向上传播。使用 ? 运算符可执行此操作。

let weather = get_weather(hometown)?;
  • 如果结果成功,会解包 Result 以获取其中的成功值。
  • 如果错误,会立即从函数返回,将错误结果沿着调用链向上传播。

在 Rust 1.13 引入 ? 运算符之前,会使用 try!() 宏。

处理多种错误类型

例如下面的代码:

use std::io::{self, BufRead};
fn read_nunmbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
    let mut numbers = vec![];
    for line_result in file.lines() {
        let line = line_result?;
        numbers.push(line.parse()?); // 报错
    }
    Ok(numbers)
}

line_result 的类型是 Result<String, std::io::Error>line.parse() 的类型是 Result<i64, std::num::ParseIntError>。而 read_numbers() 函数的返回类型只能容纳io::Error。Rust 试图将 ParseIntError 转换为 io::Error,但是无法进行这样的转换,所以得到一个错误。 要解决这个问题,**方式一是自定义自己的错误类型。可以使用 **thiserror** crate,它可以使用几行代码定义良好的错误类型。 或者使用 Rust 中的内置特性。所有标准库中的错误类型都能转换为类型 Box<dyn std::error::Error + Send + Sync + 'static>dyn std::error::Error 表示任何错误。 Send+Sync+'static 表示可以安全地在线程之间传递 。 可以通过定义类型别名来简化。

type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;

还可考虑使用 anyhow crate,它提供了错误类型和结果类型。 将 read_numbers 函数的返回值类型改为 GenericResult<Vec<i64>>。这样就能根据需要自动将任意类型的错误转换为 GenericError? 会使用 From 特型以及其 from() 方法来自动转化。也可自己使用方法来转换。

let io_error = io::Error::new(io::ErrorKind::Other, "time out");
return Err(GenericError::from(io_error)); // 手动转换为 GenericError

使用 GenericError 的缺点是返回类型不再准确的传达调用者可预期的错误类型。 如果正在调用一个返回 GenericResult 的函数,并且想要处理一种特定类型的错误,而让所有其他错误传播出去,那么可以使用泛型方法 error.downcast_ref::<ErrorType>()。如果这个错误恰好是你要找的那种类型的错误,该方法就会借用对它的引用。

loop {
	match compile_project() {
		Ok(()) => return Ok(()),
		Err(err) => {
			if let Some(mse) => err.downcast_ref::<MissingSenicolonError>() {
				insert_semicolon_in_source_code(mse.file(), mse.line())?;
				continue;
			}
			return Err(err);
		}
	}
}

处理"不可能发生"的错误

如果确信错误不可能发生,可以使用 .unwrap().expect(msg) 来进行处理,简化 Result 的处理。

忽略错误

可使用 _ 来消除。

let _ = writeln!(stderr(), "error: {}", err);

处理 main() 中的错误

错误通过传递,如果最后到达了 main() 函数,那么此时必须在 main() 函数中进行处理。 最简单的方式是使用 .expect()。 也可更改 main() 的类型签名以返回 Result 类型,这样就可以使用 ? 了。

fn main() -> Result<(), TideCalcError> {
	let tides = calculate_tides()?;
	print_tides(tides);
	Ok(())
}

还有一种方式是使用 if let

fn main() {
	if let Err(err) = calculate_tides() {
		print_error(&err);
		std::process::exit(1);
	}
}

声明自定义错误类型

自定义 JsonError 错误。

#[derive(Debug, Clone)]
pub struct JsonError {
	pub message: String,
	pub line: usize,
	pub column: usize,
}

// 错误应该能打印
use std::fmt;
impl fmt::Display for JsonError {
	fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
		write!(f, "{} ({}:{})", self.message, self.line, self.column)
	}
}

// 错误应该实现 std::error::Error 特型,但使用 Error 各个方法的默认定义就够了
impl std::error::Error for JsonError {}

可以使用 thiserror crate 来简化上面的操作。

use thiserror::Error;

#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")]
pub struct JsonDrror {
	message: String,
	line: usize,
	column: usize
}

为什么优先选择 Result?

为什么 Rust 会优先选择 Result 而非直接触发 panic。

  • Rust 要求程序员在每个可能发生错误的地方做决策,并将其记录在代码中。
  • 最常见的决策是让错误继续传播,使用 ? 实现。
  • 是否可能出错是每个函数返回类型的一部分。
  • Rust 会检查 Result 值是否被用过,这样就不会意外地让错误悄悄溜过去。
  • Result 是一种数据类型,将成功结果和错误结果存储在同一个集合中。

参考链接:

🌟 🙏🙏感谢您的阅读,如果对您有帮助,欢迎关注、点赞 🌟🌟