错误处理
目录
基本的错误处理
如果你不能使用问号操作符,那么在 Rust 中的错误处理会很笨拙。
为了这种实现的快乐,我们需要返回一个可以接受任何错误的Result
。 所有错误都会实现std::error::Error
trait,这样 任何 错误都可以转换成一个Box<Error>
。
说我们需要处理 I/O 错误和从 String 转换到数字的 两种 错误:
# #![allow(unused_variables)] #fn main() { // box-error.rs use std::fs::File; use std::io::prelude::*; use std::error::Error; fn run(file: &str) -> Result<i32,Box<Error>> { let mut file = File::open(file)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; // Result<usize> Ok(contents.trim().parse()?) } #}
所以,这给出了的两个问号,一个给 I/O 错误 (无法打开文件,或无法读取为 String) 以及转换错误一个。 最后,我们将结果包装在Ok
内。Rust 可以根据返回类型签名,从parse
得出应转换为i32
。
很容易为Result
类型创建一个简写:
# #![allow(unused_variables)] #fn main() { type BoxResult<T> = Result<T,Box<Error>>; #}
但是,我们的程序将具有特定于应用程序的错误条件,还需要创建自己的错误类型。错误类型的基本要求也很简单:
- 可以 impl
Debug
- 必须 impl
Display
- 必须 impl
Error
还有啊,你的错误可以做它喜欢做的事情。
# #![allow(unused_variables)] #fn main() { // error1.rs use std::error::Error; use std::fmt; #[derive(Debug)] struct MyError { details: String } impl MyError { fn new(msg: &str) -> MyError { MyError{details: msg.to_string()} } } impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f,"{}",self.details) } } impl Error for MyError { fn description(&self) -> &str { &self.details } } // 一个返回我们错误结果的测试函数 fn raises_my_error(yes: bool) -> Result<(),MyError> { if yes { Err(MyError::new("borked")) } else { Ok(()) } } #}
老输入Result<T,MyError>
会乏味的,许多 Rust 模块会定义它们自己的Result
- 例如io::Result<T>
是Result<T,io::Error>
的简写。
在下一个例子中,当一个 String 不能被解析为一个浮点数时,我们需要处理特定的错误。
现在?
工作的方式,是从 表达 的错误到必 返回 的错误的一种转换。 并且这个转换由From
trait 表示。Box<Error>
一样是这样工作的,因为它为所有实现了Error
的类型实现From
。
此时您可以继续使用便捷的别名BoxResult
,像以前一样 catch 所有事情; 会有一个我们的错误到Box<Error>
的转换,这对小型应用程序来说是一个很好的选择。 但我想显示其他错误,明确与我们的错误类型的合作。
ParseFloatError
实现了 Error
, 所以description()
方法可用。
# #![allow(unused_variables)] #fn main() { use std::num::ParseFloatError; impl From<ParseFloatError> for MyError { fn from(err: ParseFloatError) -> Self { MyError::new(err.description()) } } // and test! fn parse_f64(s: &str, yes: bool) -> Result<f64,MyError> { raises_my_error(yes)?; let x: f64 = s.parse()?; Ok(x) } #}
第一个?
还行 (一种类型总用From
转换自己) 和第二个?
将转换ParseFloatError
到MyError
。
结果如下:
fn main() { println!(" {:?}", parse_f64("42",false)); println!(" {:?}", parse_f64("42",true)); println!(" {:?}", parse_f64("?42",false)); } // Ok(42) // Err(MyError { details: "borked" }) // Err(MyError { details: "invalid float literal" })
不会太复杂,就有点啰嗦。 该繁琐处是不得不为所有其他需要与MyError
玩耍的错误类型,编写From
- 或者简单点,依靠Box<Error>
。 新手会因为多种方式在 Rust 中做同样的事情而感到困惑; 总是有另一种方法帮鳄梨削皮。代价有很多灵活选择。 200 行的错误处理程序可比大型应用程序简单得多。若您想将您的’宝贝’打包为一个 Cargo crate,那么错误处理就变得至关重要。
目前,问号运算符仅适用于Result
,不是Option
,这是一个功能,而不是一个限制。 Option
有一个ok_or_else
,该方法将自己转换成一个Result
。例如说,我们有一个HashMap
,若没有定义键的话,则必须失败:
# #![allow(unused_variables)] #fn main() { let val = map.get("my_key").ok_or_else(|| MyError::new("my_key not defined"))?; #}
现在这里返回的错误是很清楚的! (该形式 使用闭包,因此只有在查找失败时才会创建错误值。)
提供简单错误的 simeple-error
该simple-errorcrate 为你提供基于一个字符串 的基本错误类型,正如我们在这里定义的那样,以及一些方便的宏。如同其他任何错误一样,Box<Error>
也可以正常工作:
#[macro_use] extern crate simple_error; use std::error::Error; type BoxResult<T> = Result<T,Box<Error>>; fn run(s: &str) -> BoxResult<i32> { if s.len() == 0 { bail!("empty string"); } Ok(s.trim().parse()?) } fn main() { println!("{:?}", run("23")); println!("{:?}", run("2x")); println!("{:?}", run("")); } // Ok(23) // Err(ParseIntError { kind: InvalidDigit }) // Err(StringError("empty string"))
bail!(s)
宏扩展为return SimpleError::new(s).into();
- 提前返回转换 成 接收的类型签名。
你需要使用BoxResult
,混合SimpleError
类型与其他错误,因为我们无法为它实现From
, 因为它的 trait 和类型都来自其他箱子(安全问题)。
提供严重错误的 error-chain
非凡的应用程序,看过来error_chaincrate。Rust 的一个小宏魔法的漫漫长路。
创建一个二进制包cargo new --bin test-error-chain
,并进到这个目录。 编辑Cargo.toml
,添加error-chain="0.8.1"
到最后。
error-chain 为你做的是什么, 创建我们所需的所有定义的手动执行错误类型; 创建一个结构体,并实现必要的 trait : Display
,Debug
和Error
,也默认实现 From
, 所以字符串 可以转换成错误。
我们的src/main.rs
文件看起来像这样。所有的主要程序都是给run
调用,打印出错误,并用非零退出代码结束程序。 error_chain
宏,会在定义error
的模块里面生成所有所需的 - 在一个更大的程序中,你会把error
的模块放在它自己的文件中。 我们需要把放进error
的所有东西,带回到全局作用域,因为我们的代码需要生成的 traits。 默认情况下,随带有一个Error
结构和一个Result
的定义。
我们也要求From
的实现,这样使用foreign_links
,std::io::Error
才会转换为我的错误类型:
#[macro_use] extern crate error_chain; mod errors { error_chain!{ foreign_links { Io(::std::io::Error); } } } use errors::*; fn run() -> Result<()> { use std::fs::File; File::open("file")?; Ok(()) } fn main() { if let Err(e) = run() { println!("error: {}", e); std::process::exit(1); } } // error: No such file or directory (os error 2)
‘foreign_links’让我们的生活更轻松,因为问号符号现在知道如何转换std::io::Error
进入我们的error::Error
。 (在引擎盖下,宏正在创建一个From<std::io::Error>
转换实现,正如前面所述。 )
所有的行动都发生在run
;让我们打印出作为第一个程序参数给出的文件的前 10 行。 有可能或不会有这样的参数,这不一定是错误的。 这里我们要转换一个Option<String>
到一个Result<String>
。Option
有两个做这种转换的方法,我选择了最简单的一种。 我们的Error
类型为&str
实现From
,所以用一个简单的文本就可以很容易制作一个错误。
# #![allow(unused_variables)] #fn main() { fn run() -> Result<()> { use std::env::args; use std::fs::File; use std::io::BufReader; use std::io::prelude::*; let file = args().skip(1).next() .ok_or(Error::from("provide a file"))?; let f = File::open(&file)?; let mut l = 0; for line in BufReader::new(f).lines() { let line = line?; println!("{}", line); l += 1; if l == 10 { break; } } Ok(()) } #}
(再次) 有一个有用的小宏bail!
,用于’抛出’错误。ok_or
方法的一个替代方案:
# #![allow(unused_variables)] #fn main() { let file = match args().skip(1).next() { Some(s) => s, None => bail!("provide a file") }; #}
会像?
一样,它 提前返回。
返回的错误包含一个ErrorKind
枚举,这使我们能够区分各种各样的错误。 总有一个Msg
变体 (当你用Error::from(str)
) 和foreign_links
申明包装 I/O 错误的Io
:
fn main() { if let Err(e) = run() { match e.kind() { &ErrorKind::Msg(ref s) => println!("msg {}",s), &ErrorKind::Io(ref s) => println!("io {}",s), } std::process::exit(1); } } // $ cargo run // msg provide a file // $ cargo run foo // io No such file or directory (os error 2)
添加新的错误很简单。 添加一个Error
部分给error_chain!
宏:
# #![allow(unused_variables)] #fn main() { error_chain!{ foreign_links { Io(::std::io::Error); } errors { NoArgument(t: String) { display("no argument provided: '{}'", t) } } } #}
这定义了Display
如何应用在这种新的错误。 现在我们可以更具体地处理’no argument’的错误,喂给ErrorKind::NoArgument
一个String
值:
# #![allow(unused_variables)] #fn main() { let file = args().skip(1).next() .ok_or(ErrorKind::NoArgument("filename needed".to_string()))?; #}
现在有一个额外的,您必须匹配的ErrorKind
变体:
fn main() { if let Err(e) = run() { println!("error {}",e); match e.kind() { &ErrorKind::Msg(ref s) => println!("msg {}", s), &ErrorKind::Io(ref s) => println!("io {}", s), &ErrorKind::NoArgument(ref s) => println!("no argument {:?}", s), } std::process::exit(1); } } // cargo run // error no argument provided: 'filename needed' // no argument "filename needed"
一般来说,尽可能使错误尽可能具有特定的意义,尤其 如果是一个库函数! 这种 match-on-kind 技术几乎相当于传统的异常处理,您可以在catch
要么except
块种匹配异常类型。
综上所述,error-chain为你创建一个类型Error
,std::result::Result<T,Error>
定义为Result<T>
。 Error
包含一个枚举ErrorKind
,并且默认情况下有一个变体Msg
用于从 String 创建的错误。 你用foreign_links
来定义外部错误,这有两件事。首先,它创建一个新的ErrorKind
变种。 其次,它在这些外部错误上实现了From
,所以他们可以转换成我们的错误。新的错误变体很容易地添加。许多恼人的样板代码被淘汰。
错误的链化
但这个箱子提供的非常酷的东西是 error-链化.
作为一个 用户 ,当一个方法只是’抛出’一个通用的 I/O 错误时,这是烦人的。 好吧,它不能打开一个文件,很好,但这又是什么文件? 简单点来说,这个信息对我有什么用处?
error_chain
给出了 error-链化 答案, 这有助于解决过度通用错误的问题。 当我们尝试打开文件时,我们可以懒洋洋地用?
,看着它变成io::Error
, 或者你可以选择 链化 这错误。
# #![allow(unused_variables)] #fn main() { // 普通错误 let f = File::open(&file)?; // 一个特殊的错误链 let f = File::open(&file).chain_err(|| "unable to read the damn file")?; #}
这里是该程序的新版本, 没有 导入’foreign’错误,只是默认值:
#[macro_use] extern crate error_chain; mod errors { error_chain!{ } } use errors::*; fn run() -> Result<()> { use std::env::args; use std::fs::File; use std::io::BufReader; use std::io::prelude::*; let file = args().skip(1).next() .ok_or(Error::from("filename needed"))?; ///////// 显式链化! /////////// let f = File::open(&file).chain_err(|| "unable to read the damn file")?; let mut l = 0; for line in BufReader::new(f).lines() { let line = line.chain_err(|| "cannot read a line")?; println!("{}", line); l += 1; if l == 10 { break; } } Ok(()) } fn main() { if let Err(e) = run() { println!("error {}", e); /////// 查看错误链... /////// for e in e.iter().skip(1) { println!("caused by: {}", e); } std::process::exit(1); } } // $ cargo run foo // error unable to read the damn file // caused by: No such file or directory (os error 2)
所以chain_err
方法接受原始错误,并创建一个包含原始错误的新错误 - 这可以无限期地持续下去。 这个闭包函数期待那些能 转换 为错误的值。
Rust 宏可以明显地为您节省大量的打字工作。 error-chain
甚至提供了一个取代整个主程序的捷径:
quick_main!(run);
(run
就是所有行动的地点,无需管其他。 )