Rust中的错误处理 如果你使用过其他编程语言,那么就会知道Rust中的错误处理是完全不同的方法。像Java,JS,Python等你常常会使用throw
处理异常,以及return
成功的值。但在Rust中,你会返回给调用者一个Result
Result<T, E>
是一个拥有两个值的枚举类型,其中Ok(T)
用来返回成功值,Err(E)
用来返回错误值
1 2 3 4 enum Result <T, E> { Ok (T), Err (E), }
返回错误而不是抛出错误,这是一种编程习惯的转变。所以如果你是Rust的初学者,可能刚开始学习起来觉得很麻烦,因为这需要你用去思考在不同的场景,应该使用什么样的方法去处理错误。
在这篇博客中会有错误处理的一些范式,以及它们在Rust中是如何体现的:
忽略错误
直接结束程序
使用默认值处理
传递错误
传递多个错误
模式匹配Boxed错误
使用库 or 应用
创建自定义错误
传递自定义错误
模式匹配自定义错误
忽略错误 (unwrap())(如果出现错误会触发panic,让该线程退出)
最简单的处理方法就是直接忽略这个错误,这听起来是不太好的想法,但是可以在以下情况使用:
刚刚开始编写代码,不想浪费太多时间在错误处理上。
坚定确信当前的情况下,错误一定不会发生。
1 2 3 4 5 6 use std::fs;fn main () { let content = fs::read_to_string ("./Cargo.toml" ).unwrap (); println! ("{}" , content) }
即使知道文件会存在,但是编译器也无法知道。因此,使用unwrap()
关键字让编译器信任,并返回其中的值。如果read_to_string()
函数返回一个Ok()
,unwrap将获取Ok()
的内容并将其分配给content变量。如果它返回一个错误,那么程序会陷入panic
这回让当前程序线程退出。
需要注意的是在许多Rust示例代码中使用unwrap
来跳过错误处理,但是这样做主要是为了方便,不应该在实际开发中使用。
结束程序 (expect())有些错误无法处理或从中恢复。在这些情况下,最好直接终止程序。 让使用与上面相同的例子——正在读取一个文件,肯定会看到它。想象一下,对于这个程序来说,这个文件绝对重要,没有它就无法正常工作。如果由于某种原因,该文件不存在,那么最好应该直接终止该程序。
可以像之前一样使用unwrap
或者使用expect
,它和unwrap
差不多,唯一不同的是添加了额外的错误信息。
1 2 3 4 5 6 use std::fs;fn main () { let content = fs::read_to_string ("./Cargo.toml" ).expect ("Can't read Cargo.toml" ); println! ("{}" , content) }
了解关于:panic!
使用默认值 (unwrap_or())在某些情况下,可以通过返回默认值来处理错误。
例如正在编写一个服务器,它监听的端口可以使用环境变量进行配置。如果没有设置环境变量,则访问该值将导致错误。但可以通过返回默认值来轻松处理这个问题。
1 2 3 4 5 6 use std::env;fn main () { let port = env::var ("PORT" ).unwrap_or ("3000" .to_string ()); println! ("{}" , port); }
在这里,使用在这里,使用了一种称为unwrap_or
的unwrap
变体,它允许提供默认值。
了解关于: unwrap_or_else
, unwrap_or_default
传递错误给调用者 当没有足够的context来处理错误时,可以将错误冒泡(向上传播)到调用者函数。下面是一个精心设计的示例,它使用Web服务获取当前年份:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 use std::collections::HashMap;fn main () { match get_current_date () { Ok (date) => println! ("We've time travelled to {}!!" , date), Err (e) => eprintln! ("Oh noes, we don't know which era we're in! :( \n {}" , e), } } fn get_current_date () -> Result <String , reqwest::Error> { let url = "https://postman-echo.com/time/object" ; let result = reqwest::blocking::get (url); let response = match result { Ok (res) => res, Err (err) => return Err (err), }; let body = response.json::<HashMap<String , i32 >>(); let json = match body { Ok (json) => json, Err (err) => return Err (err), }; let date = json["years" ].to_string (); Ok (date) }
这块在get_current_date
中有两个函数调用(get和json)会返回Result值。因为get_current_date并没有返回错误时要做什么的上下文,所以使用模式匹配将错误传回main.
有时候在使用模式匹配去处理,多个嵌套的错误处理可能会让代码看起来非常混乱,所以可以引入?
来重写上述代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use std::collections::HashMap;fn main () { match get_current_date () { Ok (date) => println! ("We've time travelled to {}!!" , date), Err (e) => eprintln! ("Oh noes, we don't know which era we're in! :( \n {}" , e), } } fn get_current_date () -> Result <String , reqwest::Error> { let url = "https://postman-echo.com/time/object" ; let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; let date = res["years" ].to_string (); Ok (date) }
这看起来简洁明了。其中?
操作符类似于unwrap
但是在遇到Error时并不会产生panic
,而是会将错误返回给调用者函数。需要记住的一件事情是只有在函数返回Option
和Result
类型时才能使用?
操作符。
Rust语言中Option和Result两种类型的使用_rust option result-CSDN博客
注意Option和Result:
1 2 3 4 5 6 7 8 9 10 pub enum Option <T> { None , Some (T), } pub enum Result <T, E> { Ok (T), Err (E), }
其中Option更多用于一个返回值不确定是否存在的情况下(可以理解为其他语言中的NULL)。
而Result更多用于会出现错误进行捕捉的场景。
所以会看到很多函数后面的返回值(可以return,也可以直接None/Some/Ok/Err)是这样的,给出各自的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 fn find_element_index (arr: &[i32 ], target: i32 ) -> Option <usize > { for (index, &value) in arr.iter ().enumerate () { if value == target { return Some (index); } } None } fn divide (a: i32 , b: i32 ) -> Result <i32 , &'static str > { if b == 0 { Err ("Cannot divide by zero" ) } else { Ok (a / b) } } fn main () { let numbers = [1 , 2 , 3 , 4 , 5 ]; let index = find_element_index (&numbers, 3 ); match index { Some (i) => println! ("Element found at index: {}" , i), None => println! ("Element not found" ), } match divide (10 , 2 ) { Ok (result) => println! ("Result of division: {}" , result), Err (error) => println! ("Error: {}" , error), } divide (10 ,2 ).unwrap (); divide (10 ,2 ).expect ("defined by yourself" ); }
传递多个错误给调用者 在之前的例子中,get
和json
函数返回了一个reqwest::Error
错误。但是如果已经有了一个在调用其他的函数时返回的错误类型,那么应该怎么处理呢?
让通过返回格式化的日期而不是年份来扩展上一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + use chrono::NaiveDate; use std::collections::HashMap; fn main () { match get_current_date () { Ok (date) => println! ("We've time travelled to {}!!" , date), Err (e) => eprintln! ("Oh noes, we don't know which era we're in! :( \n {}" , e), } } fn get_current_date () -> Result <String , reqwest::Error> { let url = "https://postman-echo.com/time/object" ; let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; - let date = res["years" ].to_string (); + let formatted_date = format! ("{}-{}-{}" , res["years" ], res["months" ] + 1 , res["date" ]); + let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" )?; + let date = parsed_date.format("%Y %B %d" ).to_string (); Ok (date) }
上述的代码是不能编译的,由于parse_from_str返回了一个chrono::format::ParseError
错误而不是reqwest::Error
可以使用Box
关键字来解决这个问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use chrono::NaiveDate; use std::collections::HashMap; fn main () { match get_current_date () { Ok (date) => println! ("We've time travelled to {}!!" , date), Err (e) => eprintln! ("Oh noes, we don't know which era we're in! :( \n {}" , e), } } - fn get_current_date () -> Result <String , reqwest::Error> { + fn get_current_date () -> Result <String , Box <dyn std::error::Error>> { let url = "https://postman-echo.com/time/object" ; let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; let formatted_date = format! ("{}-{}-{}" , res["years" ], res["months" ] + 1 , res["date" ]); let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" )?; let date = parsed_date.format("%Y %B %d" ).to_string (); Ok (date) }
当想要发返回多个不同的错误时,返回一个特征对象 Box<dyn std::error::Error>
是一种便利的处理方法。
了解更多关于: anyhow
, eyre
模式匹配Boxed错误 目前为止,只在main中打印错误,而不是真正地去处理它们。如果想处理和恢复Box错误,需要“downcast”它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 use chrono::NaiveDate; use std::collections::HashMap; fn main () { match get_current_date () { Ok (date) => println! ("We've time travelled to {}!!" , date), - Err (e) => eprintln! ("Oh noes, we don't know which era we're in! :( \n {}" , e), + Err (e) => { + eprintln! ("Oh noes, we don't know which era we're in! :(" ); + if let Some (err) = e.downcast_ref::<reqwest::Error>() { + eprintln! ("Request Error: {}" , err) + } else if let Some (err) = e.downcast_ref::<chrono::format::ParseError>() { + eprintln! ("Parse Error: {}" , err) + } + } } } fn get_current_date () -> Result <String , Box <dyn std::error::Error>> { let url = "https://postman-echo.com/time/object" ; let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; let formatted_date = format! ("{}-{}-{}" , res["years" ], res["months" ] + 1 , res["date" ]); let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" )?; let date = parsed_date.format("%Y %B %d" ).to_string (); Ok (date) }
需要注意的是,必须知道get_current_date的实现细节(其中包含的不同错误),才能够在main中对其进行downcast。
了解更多关于: downcast
, downcast_mut
库 vs 应用 如前面所述,使用Box带来的问题是:如果想要处理底层错误,必须了解函数的实现细节。当以Box< dyn std::error::Error>
形式返回某个内容时,具体的类型信息将会丢失。为了以不同方式处理不同的错误,需要将它们向下转换为某个具体类型,这种转换可能会在运行时失败。
然而,脱离上下文,谈论好坏并不是很有用。一个很好的经验法则是思考当前正在编写的代码是一个“Application”还是“Library”:
Application应用
您正在编写的代码将由最终用户使用。
大多数由应用程序代码生成的错误不会被处理,而是记录或报告给用户。
可以使用box错误。
Library库
您正在编写的代码将被其他代码使用。一个“库”可以是开源crate,内部library等。
错误是库的API的一部分,因此库的使用者知道应该期望并从中恢复哪些错误。
库中的错误通常由用户处理,因此它们需要结构化且易于执行exhaustive match
如果您返回Box错误,那么库的使用者需要知道由代码、依赖项等创建的错误类型!
可以返回自定义错误,而不是Box错误。
创建自定义错误 对于library代码,可以将所有错误转换为自定义的错误类型,并返回它,而不是使用特征对象box
。在的例子中,目前由两个错误reqwest::Error
和chrono::format::ParseError
。可以将它们分别转换为MyCustomError::HttpError
和MyCustomError::ParseError
首先要创建一个enum来装的两个错误变量
1 2 3 4 5 6 pub enum MyCustomError { HttpError, ParseError, }
Error
trait规定实现必须Debug
和Display
traits:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use std::fmt;#[derive(Debug)] pub enum MyCustomError { HttpError, ParseError, } impl std ::error::Error for MyCustomError {}impl fmt ::Display for MyCustomError { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { match self { MyCustomError::HttpError => write! (f, "HTTP Error" ), MyCustomError::ParseError => write! (f, "Parse Error" ), } } }
就这样创建的自定义错误类型,这是一个非常简单的例子,但是没有包含太多关于错误的信息。但这应该足以作为创建更复杂、更现实的自定义错误的起点。下面是一些常见开发中的自定义错误处理的例子:ripgrep , reqwest , csv and serde_json
还有: thiserror
, snafu
传递自定义错误 看看使用自定义错误类型后对于之前程序的修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 + mod error; use chrono::NaiveDate; + use error::MyCustomError; use std::collections::HashMap; fn main () { } - fn get_current_date () -> Result <String , Box <dyn std::error::Error>> { + fn get_current_date () -> Result <String , MyCustomError> { let url = "https://postman-echo.com/time/object" ; - let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; + let res = reqwest::blocking::get (url) + .map_err (|_| MyCustomError::HttpError)? + .json::<HashMap<String , i32 >>() + .map_err (|_| MyCustomError::HttpError)?; let formatted_date = format! ("{}-{}-{}" , res["years" ], res["months" ] + 1 , res["date" ]); - let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" )?; + let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" ) + .map_err (|_| MyCustomError::ParseError)?; let date = parsed_date.format("%Y %B %d" ).to_string (); Ok (date) }
注意到使用了map_err
将一个错误类型转换为另外的错误类型.
但是可以看到太多的verbose作为结果,的函数充斥着太多map_err
调用。可以实现From
trait,这样就会完成自动错误类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 use std::fmt; #[derive(Debug)] pub enum MyCustomError { HttpError, ParseError, } impl std ::error::Error for MyCustomError {} impl fmt ::Display for MyCustomError { fn fmt (&self , f: &mut fmt::Formatter) -> fmt::Result { match self { MyCustomError::HttpError => write! (f, "HTTP Error" ), MyCustomError::ParseError => write! (f, "Parse Error" ), } } } + impl From <reqwest::Error> for MyCustomError { + fn from (_: reqwest::Error) -> Self { + MyCustomError::HttpError + } + } + impl From <chrono::format::ParseError> for MyCustomError { + fn from (_: chrono::format::ParseError) -> Self { + MyCustomError::ParseError + } + } mod error; use chrono::NaiveDate; use error::MyCustomError; use std::collections::HashMap; fn main () { } fn get_current_date () -> Result <String , MyCustomError> { let url = "https://postman-echo.com/time/object" ; - let res = reqwest::blocking::get (url) - .map_err (|_| MyCustomError::HttpError)? - .json::<HashMap<String , i32 >>() - .map_err (|_| MyCustomError::HttpError)?; + let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; let formatted_date = format! ("{}-{}-{}" , res["years" ], res["months" ] + 1 , res["date" ]); - let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" ) - .map_err (|_| MyCustomError::ParseError)?; + let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" )?; let date = parsed_date.format("%Y %B %d" ).to_string (); Ok (date) }
在移除map_err
后代码变得更加整洁了。
然而,From
trait并不是一种能减少使用map_err
的魔法。在上面的例子中,将类型转换从get_current_data
函数内部移动到From<X> for MyCustomError
实现.那么如果一个错误没有在MyCustomError
出现过,就不能使用From trait,从而只能使用map_err
模式匹配自定义错误 一直忽略了main的变动,现在要看看如何去处理自定义错误类型的模式匹配问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 mod error; use chrono::NaiveDate; use error::MyCustomError; use std::collections::HashMap; fn main () { match get_current_date () { Ok (date) => println! ("We've time travelled to {}!!" , date), Err (e) => { eprintln! ("Oh noes, we don't know which era we're in! :(" ); - if let Some (err) = e.downcast_ref::<reqwest::Error>() { - eprintln! ("Request Error: {}" , err) - } else if let Some (err) = e.downcast_ref::<chrono::format::ParseError>() { - eprintln! ("Parse Error: {}" , err) - } + match e { + MyCustomError::HttpError => eprintln! ("Request Error: {}" , e), + MyCustomError::ParseError => eprintln! ("Parse Error: {}" , e), + } } } } fn get_current_date () -> Result <String , MyCustomError> { let url = "https://postman-echo.com/time/object" ; let res = reqwest::blocking::get (url)?.json::<HashMap<String , i32 >>()?; let formatted_date = format! ("{}-{}-{}" , res["years" ], res["months" ] + 1 , res["date" ]); let parsed_date = NaiveDate::parse_from_str (formatted_date.as_str (), "%Y-%m-%d" )?; let date = parsed_date.format("%Y %B %d" ).to_string (); Ok (date) }
请注意,与Boxed错误不同,实际上可以直接匹配MyCustomError enum中的变量。