跳转到内容

Rust 自定义错误类型

在 Result 那一页,每个错误都只是一个 String。对小例子来说这没问题,但一旦一个函数能以不止一种方式失败,它就不够用了:String 没法用来做模式匹配,调用者想知道到底是哪一种失败,只能去解析错误文本本身。

fn withdraw(balance: f64, amount: f64) -> Result<f64, String> {
if amount > balance {
return Err(String::from("余额不足"));
}
if amount < 0.0 {
return Err(String::from("金额不能为负数"));
}
Ok(balance - amount)
}

如果调用者想对“余额不足”和“金额为负”做出不同的反应,唯一的办法就是比较错误字符串本身。这很脆弱:只要你改一下消息的措辞,它就坏了。

枚举能解决这个问题:每种失败都成为一个单独的变体,调用者可以直接对它 match:

enum WithdrawError {
InsufficientFunds,
NegativeAmount,
}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> {
if amount < 0.0 {
return Err(WithdrawError::NegativeAmount);
}
if amount > balance {
return Err(WithdrawError::InsufficientFunds);
}
Ok(balance - amount)
}
fn main() {
match withdraw(100.0, 150.0) {
Ok(remaining) => println!("新余额:{remaining}"),
Err(WithdrawError::InsufficientFunds) => println!("钱不够"),
Err(WithdrawError::NegativeAmount) => println!("不能取出负数金额"),
}
}

输出:

钱不够
▶ 动手试一试

现在编译器会确保每一种失败都被处理到:这和 match 一直以来对枚举的保证一样,只不过用在了错误上。

实现 Display,给出像样的错误信息

Section titled “实现 Display,给出像样的错误信息”

现在就试着用 {} 打印 WithdrawError,会编译不过:Rust 不知道该怎样把它变成文字。实现 Display trait 就能解决。trait 很快会有专门的页面(trait 和常用 trait),现在你可以先照着这个模式写:

use std::fmt;
enum WithdrawError {
InsufficientFunds,
NegativeAmount,
}
impl fmt::Display for WithdrawError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
WithdrawError::InsufficientFunds => write!(f, "余额不足"),
WithdrawError::NegativeAmount => write!(f, "金额不能为负数"),
}
}
}
fn main() {
let error = WithdrawError::InsufficientFunds;
println!("错误:{error}");
}

输出:

错误:余额不足
▶ 动手试一试

现在 WithdrawError 就像其他任何错误一样,可以打印给用户看或者写进日志;同时,需要针对某种具体情况做出反应的代码,依然可以对它完整地 match。

在真实的项目里,手写 Display 以及自定义错误类型周围的样板代码,很快就会变得重复。thiserror(英文)crate 用一个 #[derive] 就能帮你生成其中的大部分;anyhow(英文)则在应用程序代码中很流行,适合只想传播错误、不想给每种错误都定义一个类型的场景。知道有它们就好,学习基础知识时还用不到。等你准备好了,anyhow 与 thiserror 那一页会演示这两个 crate,并讲清楚什么时候该选哪一个。

定义一个枚举 LoginError,它有两个变体:EmptyUsername 和 PasswordTooShort,并为它实现 Display。然后写一个函数 fn check_login(user: &str, password: &str) -> Result<String, LoginError>,成功时返回带欢迎语的 Ok,否则返回对应的错误。密码至少要有 8 个字符。提示:&str 的 .is_empty() 和 .len() 就够你做检查了。

查看答案
use std::fmt;
enum LoginError {
EmptyUsername,
PasswordTooShort,
}
impl fmt::Display for LoginError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LoginError::EmptyUsername => write!(f, "用户名不能为空"),
LoginError::PasswordTooShort => write!(f, "密码至少需要 8 个字符"),
}
}
}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> {
if user.is_empty() {
return Err(LoginError::EmptyUsername);
}
if password.len() < 8 {
return Err(LoginError::PasswordTooShort);
}
Ok(format!("欢迎,{user}!"))
}
fn main() {
let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts {
match check_login(user, password) {
Ok(message) => println!("{message}"),
Err(e) => println!("错误:{e}"),
}
}
}

输出:

欢迎,ana!
错误:用户名不能为空
错误:密码至少需要 8 个字符
▶ 动手试一试

用户名和密码保留英文,因为 .len() 数的是字节:一个汉字占 3 个字节,用中文密码的话长度检查就不准了。

写一个函数 fn parse_percent(input: &str) -> Result<u32, PercentError>,其中 PercentError 有两个带数据的变体:NotANumber(String)(出错的文本)和 TooLarge(u32)(超过 100 的那个数)。让 Display 的消息包含这些详细信息。提示:枚举变体可以装值,就像枚举那一页讲的那样。在 fmt 里用 PercentError::TooLarge(n) => ... 做匹配,就能把值取出来。

查看答案
use std::fmt;
enum PercentError {
NotANumber(String),
TooLarge(u32),
}
impl fmt::Display for PercentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PercentError::NotANumber(text) => write!(f, "“{text}”不是数字"),
PercentError::TooLarge(n) => write!(f, "{n} 超过了 100"),
}
}
}
fn parse_percent(input: &str) -> Result<u32, PercentError> {
match input.parse::<u32>() {
Ok(n) => {
if n > 100 {
Err(PercentError::TooLarge(n))
} else {
Ok(n)
}
}
Err(_) => Err(PercentError::NotANumber(input.to_string())),
}
}
fn main() {
for input in ["75", "half", "250"] {
match parse_percent(input) {
Ok(n) => println!("{n}%"),
Err(e) => println!("错误:{e}"),
}
}
}

输出:

75%
错误:“half”不是数字
错误:250 超过了 100
▶ 动手试一试
  • String 错误适合小例子,但调用者没法对不同的错误分别做匹配。
  • 定义一个枚举,每种失败对应一个变体,调用者就能对具体的错误 match。
  • 实现 Display(impl fmt::Display for YourError),错误类型就能用 {} 打印。
  • 在大一些的项目里,thiserror 和 anyhow 这样的 crate 能省掉大部分样板代码。

小测验

1. 当函数能以不止一种方式失败时,为什么 String 错误类型就不够用了?

2. 是什么让 withdraw 的调用者能对 InsufficientFunds 和 NegativeAmount 做出不同的反应?

3. 为自定义错误类型实现 Display 之后,你可以做什么?

4. thiserror 和 anyhow 这类 crate 能帮上什么忙?

5. 为什么说比较原始的错误消息字符串很脆弱?

得分: 0 / 5