Rust 错误处理
Rust的错误分两种:
- 可恢复错误
- 不可恢复错误
rust提供了可恢复错误的类型Result< T,E >,与不可恢复错误时终止运行的panic!宏。
不可恢复错误与panic!
程序会在panic!宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序,这种情况大部分都发生在某个错误被检测到,但程序员却不知道该如何处理的时候。
panic的栈展开与终止 panic发生时,程序默认会开始栈展开,就是会沿着函数调用的反向顺序清理函数中的数据,为了支持这种操作,我们需要在二进制中存储许多额外信息。 除了栈展开我们还可以选择立即终止,让操作系统来进行回收工作。你可以在cargo.toml中的[profile]里设置panic=‘abort’来将panic的默认行为从栈展开切换为终止。
接下来我们尝试一下调用panic:
fn main() { panic!("crash and burn"); }
报错信息:
$ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/panic` thread 'main' panicked at 'crash and burn', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
显示了我们给的panic信息,以及panic位置。
使用panic!产生的回溯信息
略,只是展示了发生错误的回溯信息。
可恢复错误与Result
大部分错误其实都没有严重到需要整个程序停止运行的地步。例如,尝试打开文件的操作会因为文件不存在而失败。你也许会在这种情形下考虑创建新文件而不是终止程序。
Result枚举定义了两个变体——Ok和Err,如下所示:
#![allow(unused_variables)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
这里的T和E是泛型的参数。T代表了Ok变体中包含的值类型,该类型值会在执行成功时返回。E代表了Err变体中包含的错误类型,该类型值会在执行失败时返回。
我们现在来打开一个文件,它的返回值将是一个Result,我们需要使用match来处理Result
use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; }
Ok表示成功返回file,失败触发panic!。 file和error都是临时定义的变量。 通过=>操作符将变量返回给f。
匹配不同的错误
可以通过match error来确定错误类型,进而处理不同类型的错误。
use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, Err(e) => panic!("Problem creating the file: {:?}", e), }, other_error => { panic!("Problem opening the file: {:?}", other_error) } }, }; }
这里的other_error是新建的临时变量,可以获得其余的错误类型。 另外,_占位符也可以获得其他错误,但是_不是变量,无法在后面使用。
更有经验的rust开发者可能会像下面这样实现:
use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {:?}", error); }) } else { panic!("Problem opening the file: {:?}", error); } }); }
这段代码使用了闭包,之后会讨论。它没有使用match,少了很多嵌套,更加易读。
失败时触发panic的快捷方式unwrap和expect
match处理Result很详尽,但有时太麻烦。可以使用unwrap方法来简化,在Ok时放回Ok内部值,在Err时调用panic!:
use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); }
加入不存在hello.txt文件,我们运行了这段代码,将会报出如下错误:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
还有一个expect方法,它的作用是:它允许我们在unwrap的基础上指定panic!所附带的错误提示信息。使用expect并附带上一段清晰的错误提示信息可以阐明意图,更容易追踪到panic:
use std::fs::File; fn main() { let f = File::open("hello.txt").expect("Failed to open hello.txt"); }
以下是错误信息:
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
传播错误
有时编写的函数出现了错误,可以把错误返回给调用者,让他们决定应该如何做进一步的处理。 我们需要将返回值设定为Result,并明确什么时候返回Ok,什么时候返回Err。
#![allow(unused_variables)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } }
_占位符又出现了,因为这一次不需要再Ok里面声明变量,所以直接用 _占位符替代。
传播错误的快捷方式 :?运算符
#![allow(unused_variables)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } }
?运算符可以快速地将错误返回。(unwrap是快速处理错误,?是快速返回错误) 假如这个Result的值是Ok,那么包含再Ok中的值就会作为这个表达式的结果返回,假如是Err,那么就会返回这个Err值。
不过,match表达式和?运算符的一个区别: 被?运算符所接收的错误会被隐式地被from函数处理,这个函数定义与From trait中,用于错误类型之间地转换。当?运算符调用from函数时,它就开始尝试将传入的错误类型转换为当前函数地返回错误类型。当一个函数拥有不同的失败原因,却使用了同一的错误返回类型来同事进行表达时,这个功能会十分有用。只要每个错误类型都实现了转换为返回错误类型的from函数,?运算符就会自动帮我们处理所有的转换过程。
?运算符消除了大量模板代码,我们甚至可以通过链式方法来调用进一步简化代码:
#![allow(unused_variables)] fn main() { use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } }
这个代码的作用和之前的代码功能一样,但代码量瞬间减少了。
?运算符只能用于返回Result的函数
?运算发只能用于返回Result的函数或实现了std::ops::Try的类型。(因为时返回错误的快捷方式啊,不能返回错误就没意义了)
另外,还有一种Result:
use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let f = File::open("hello.txt")?; Ok(()) }
这里的Box< dyn Error >被称为trait对象,之后讨论,你可以简单理解为“任何可能的错误类型”。
要不要使用panic!
只要你能确定自己可以代替调用者确定某种情形时不可恢复错误,就可以嗲用panic!。 如果你选择返回Result,你就将选择权交给了调用者。
对于某些不太常见的场景,直接触发panic!要比返回Result更合适。
示例、原型和测试
示例添加Result导致降低可读性,因为match代码太长了。 原型中往往无法确定错误处理方式,只需panic做标记 测试更应该暴露panic,即使这个方法不是需要测试的内容。
你比编译器拥有更多信息
当你能明确逻辑上不可能出错,就放心panic。这里使用unwrap直接处理。
fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); }
错误处理的指导原则
某个错误可能会使代码处于损坏状态,这时就使用panic。当某些非法的值,自相矛盾的值,或不存在的值被传入代码,且满足下列条件时,使用panic:
- 虽坏状态并不包括于其中偶尔发生的事情。
- 随后的代码无法在出现损坏状态后继续正常运行。
- 没有合适的方法来将“处于损坏状态”这一信息编码至我们所使用的类型中。
如果错误时可预期的,应该使用Result而不是panic。
创建自定义类型来进行有效性验证
略
下一章:Rust 生命周期
生命周期时一种泛型。普通泛型可以确保类型拥有期望的行为,生命周期能够确保引用在我们的使用过程中一直有效。 Rust的每个引用都有自己的生命周期,它对应着引用保持有效性的作用域。 在大多时候,生命周期都是隐式的且可以被推导 ...