Rust 自定义错误类型
在 Result 那一页,每个错误都只是一个 String。对小例子来说这没问题,但一旦一个函数能以不止一种方式失败,它就不够用了:String 没法用来做模式匹配,调用者想知道到底是哪一种失败,只能去解析错误文本本身。
String 错误的问题
Section titled “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)}如果调用者想对“余额不足”和“金额为负”做出不同的反应,唯一的办法就是比较错误字符串本身。这很脆弱:只要你改一下消息的措辞,它就坏了。
用枚举定义错误
Section titled “用枚举定义错误”枚举能解决这个问题:每种失败都成为一个单独的变体,调用者可以直接对它 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。
什么时候可以借助 crate
Section titled “什么时候可以借助 crate”在真实的项目里,手写 Display 以及自定义错误类型周围的样板代码,很快就会变得重复。thiserror(英文)crate 用一个 #[derive] 就能帮你生成其中的大部分;anyhow(英文)则在应用程序代码中很流行,适合只想传播错误、不想给每种错误都定义一个类型的场景。知道有它们就好,学习基础知识时还用不到。等你准备好了,anyhow 与 thiserror 那一页会演示这两个 crate,并讲清楚什么时候该选哪一个。
1. 登录错误
Section titled “1. 登录错误”定义一个枚举 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 个字节,用中文密码的话长度检查就不准了。
2. 带详细信息的错误
Section titled “2. 带详细信息的错误”写一个函数 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 超过了 100String错误适合小例子,但调用者没法对不同的错误分别做匹配。- 定义一个枚举,每种失败对应一个变体,调用者就能对具体的错误
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
- 定义一种错误类型 — 通过例子学 Rust:一个带
Display的自定义错误类型的简短可运行示例。 - 包裹错误 — 通过例子学 Rust:下一步,一个包裹其他错误、并通过
From与?配合使用的错误枚举。 std::error::Error— 标准库文档(英文):“正式”的错误类型都会实现的标准 trait,以及它的source()方法是做什么用的。- 要不要 panic! — Rust 程序设计语言:什么时候该返回错误、什么时候该让程序停下来的指导。
- thiserror — docs.rs(英文):为你的错误枚举派生
Display和Error的 crate,让你少写很多代码。