当我们啥也不想干的时候,不如先静下心来,想个主题,动手写点东西
格式化输出
{} + Display
在讲Rust自定义错误处理之前,我们先来了解一下Rust中格式化输出的内容,为后面自定义错误处理作个铺垫。
当我们一般使用println!("{}", peter)
,println! 宏会调用 Display trait 中的 fmt 方法来获得字符串表示,从而成功打印出 peter。
rust
pub trait Display {
// Required method
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
但是下面代码中Person结构体没有实现Display这个trait会进行报错
rust
struct Person<'a> {
name: &'a str,
age: u8
}
fn main() {
let name = "Peter";
let age = 27;
let peter = Person { name, age };
// 美化打印
println!("{}", peter);
}
下面我们来实现一下这个Display trait
rust
use std::fmt;
struct Person<'a> {
name: &'a str,
age: u8
}
impl<'a> fmt::Display for Person<'a> {
// self是Person结构体的引用
// f是Formatter结构体的可变引用
// 当我们通过write!来改变f的时候,也就改变了最后println!宏输出的结果
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}今年{}岁", self.name, self.age)
}
}
fn main() {
let name = "Peter";
let age = 27;
let peter = Person { name, age };
println!("{}", peter);
}
{:?} + Debug
当你使用 {:?} 占位符时,会调用 std::fmt::Debug trait
的 fmt 方法,它提供了一种格式化类型以便于调试的方式
你可以通过在类型定义前使用 #[derive(Debug)]
属性来自动为类型派生 Debug trait 的实现。
rust
#[derive(Debug)]
struct Person<'a> {
name: &'a str,
age: u8,
}
fn main() {
let person = Person {
name: "Alice",
age: 30,
};
// 使用 Debug trait 的 fmt 方法来格式化并打印 Person 类型的实例
println!("{:?}", person);
}
错误处理
我们以最常见的读文件来作为案例
rust
fn main() {
let path = "./src/rust.txt";
match read_file(path) {
Ok(file) => println!("{}", file),
Err(err) => println!("{}", err),
}
}
fn read_file(file_path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(file_path)
}
通常我们使用Result的枚举对象作为程序的返回值,通过Result来判断其结果,我们使用match匹配的方式来获取Result的内容,判断正常(Ok)或错误(Err)。
但是这带来了一个问题,如果我们需要多个函数处理的结果,每个函数都要返回一个Result
类型,那么我们就要match多次,会造成代码臃肿和嵌套地狱的问题,作为一名优秀的程序员,我们是坚决不会允许这种事情发生的!!!
假如我们有以下场景
- 读取一文件
- 将文件内容转化为
UTF-8
格式 - 将
UTF-8
转化为U32
的数字
rust
///读取文件内容
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 转化为u32数字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
那么我们读取一个文件,进行以上操作,代码如下,可以看出代码的可读性会非常的差
rust
fn main() {
let path = "./src/rust.txt";
match read_file(path) {
Ok(file) => match to_utf8(file.as_bytes()) {
Ok(v) => match to_u32(v) {
Ok(t) => println!("num: {}", t),
Err(err) => println!("{}", err),
},
Err(err) => println!("{}", err),
},
Err(err) => println!("{}", err),
}
}
fn read_file(file_path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(file_path)
}
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
自定义错误
要想细致了解Rust的错误处理,我们需要了解std::error::Error
该trait的内部方法,部分代码如下:
rust
pub trait Error: Debug + Display {
// Provided methods
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
fn description(&self) -> &str { ... }
fn cause(&self) -> Option<&dyn Error> { ... }
fn provide<'a>(&'a self, request: &mut Request<'a>) { ... }
}
我们可以看出Error主要实现了Debug
和Display
这两个trait,还有以下4个方法
- source:当我们自定义的错误类型中包含了子错误,我们可以重写source方法,用来指定当前错误的底层来源
- 如果当前Error是低级别的Error,并没有子Error ,需要返回None。介于其本身默认有返回值None,可以不覆盖该方法。
- 如果当前Error包含子Error ,需要返回子Error :Some(err),需要覆盖该方法。
scss
pub enum StorageError {
FileHandle(FileHandleError),
ParseItem(ParseItemError),
ItemNoExist(u32),
}
impl Error for StorageError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use FileHandleError::*;
use StorageError::*;
match self {
FileHandle(source) => match source {
EnvVar(source) => Some(source),
Io(source) => Some(source),
},
ParseItem(e) => Some(e),
ItemNoExist(_) => None,
}
}
}
- description,已经被废弃了,使用display方法替代
- cause,也已经被废弃了,从rust1.33版本开始,被Error::source方法替代了
- provide:实验性的API,还在开发阶段,我们可以先不管
总结:实现一个自定义错误的步骤如下:
- 手动实现impl std::fmt::Display的trait,并实现 fmt(...)方法。
- 手动实现impl std::fmt::Debug的trait,一般直接添加注解即可:#[derive(Debug)]
- 手动实现impl std::error::Error的trait,并根据自身error级别是否覆盖std::error::Error中的source()方法。
自定义CustomError
rust
use std::error::Error;
///自定义类型 Error,实现std::fmt::Debug的trait
#[derive(Debug)]
struct CustomError {
err: ChildError,
}
///实现Display的trait,并实现fmt方法
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CustomError is here!")
}
}
///实现Error的trait,因为有子Error:ChildError,需要覆盖source()方法,返回Some(err)
impl std::error::Error for CustomError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.err)
}
}
///子类型 Error,实现std::fmt::Debug的trait
#[derive(Debug)]
struct ChildError;
///实现Display的trait,并实现fmt方法
impl std::fmt::Display for ChildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ChildError is here!")
}
}
///实现Error的trait,因为没有子Error,不需要覆盖source()方法
impl std::error::Error for ChildError {}
///构建一个Result的结果,返回自定义的error:CustomError
fn get_super_error() -> Result<(), CustomError> {
Err(CustomError { err: ChildError })
}
fn main() {
match get_super_error() {
Err(e) => {
println!("Error: {}", e);
println!("Caused by: {}", e.source().unwrap());
}
_ => println!("No error"),
}
}
println!("Error: {}", e)
时,实际上就调用了CustomError
上Display trait
中的fmt方法,
println!("Caused by: {}", e.source().unwrap())
,实际上先调用CustomError
的souce方法,返回了子错误,并且是个Some类型,所以这时候使用unwrap程序就不会崩溃了。最后打印的是子错误上的fmt方法
重写文件处理
最后,我们用自定义错误重写下文件处理,不想写了,代码放下面,大家自己看吧!!!
最最后讲一个点吧,?
运算符会尝试从Result中获取值,如果不成功,它就会接受Error,中止函数执行,并把错误传播到调用该函数的函数。同时?
操作符依赖于 From trait。如果错误类型实现了 From trait 用于从原始错误类型转换为目标错误类型,?
操作符就会使用这个实现来自动进行错误转换。
rust
use std::fmt::{Display, Formatter};
use std::io::Error as IoError;
use std::num::ParseIntError;
use std::str::Utf8Error;
fn main() -> std::result::Result<(), CustomError> {
let path = "./src/rust.txt";
let v = read_file(path)?;
let x = to_utf8(v.as_bytes())?;
let u = to_u32(x)?;
println!("num:{:?}", u);
Ok(())
}
///读取文件内容
fn read_file(path: &str) -> std::result::Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 转化为u32数字
fn to_u32(v: &str) -> std::result::Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
#[derive(Debug)]
enum CustomError {
ParseIntError(std::num::ParseIntError),
Utf8Error(std::str::Utf8Error),
IoError(std::io::Error),
}
impl std::error::Error for CustomError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
CustomError::IoError(ref e) => Some(e),
CustomError::Utf8Error(ref e) => Some(e),
CustomError::ParseIntError(ref e) => Some(e),
}
}
}
impl Display for CustomError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
CustomError::IoError(ref e) => e.fmt(f),
CustomError::Utf8Error(ref e) => e.fmt(f),
CustomError::ParseIntError(ref e) => e.fmt(f),
}
}
}
impl From<ParseIntError> for CustomError {
fn from(s: std::num::ParseIntError) -> Self {
CustomError::ParseIntError(s)
}
}
impl From<IoError> for CustomError {
fn from(s: std::io::Error) -> Self {
CustomError::IoError(s)
}
}
impl From<Utf8Error> for CustomError {
fn from(s: std::str::Utf8Error) -> Self {
CustomError::Utf8Error(s)
}
}