错误处理
rust中错误主要有2类
- recoverable error
- unrecoverable error
对于recoverable error,如文件未找到,一般我们提醒用户,然后重试即可 unrecoverable error始终是bug的征兆,比如使用超出范围的索引访问数组,因此程序会直接终止
大多数编程语言并不区分这2种错误,而统一使用异常(Exception))机制来处理。rust没有异常而使用 Result<T, E>来处理recoverable error和当遇到unrecoverable error时使用 panic!宏来停止程序执行
Unrecoverable Errors 和 panic!宏
有两种方式可触发panic
- 导致代码崩溃的操作(如数组访问越界)
- 使用
panic!宏
Unwinding 或 Aborting
默认情况下,当发生panic时,程序会开始 unwinding,即回溯并清理堆栈,然后退出。但是回溯和清理需要大量的时间,因此rust提供立即终止的(aborting)选择,即直接终止程序但不清理
如果你需要项目打包的二进制文件尽可能小,可以选择aborting替代unwinding,在Cargo.toml的 [profile]中添加如下配置
#![allow(unused)] fn main() { [profile.release] panic = 'abort' }
panic!宏
fn main() { panic!("crash and burn"); } //输出 thread 'main' panicked at src/main.rs:2:5: crash and burn note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这个例子输出2行,第一行为线程panic信息和触发panic的位置包括文件名和行号,第二行为panic的消息。在其它实际情况下,文件名和行号可能是调用 panic!宏的位置,而不是实际导致调用 panic!的行,可使用回溯(backtrace)找出导致panic的代码
使用panic! Backtrace
fn main() { let v = vec![1, 2, 3]; //访问超过范围的索引会直接panic v[99]; } //输出 thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
在c语言中,尝试访问超过数据结构范围的索引是未定义行为(undefined behavior),或许会得到内存中与数据结构中的元素相对应的位置,即使这个内存不属于它。这种现象称为buffer overread,而且如果攻击者能够操纵索引来读取数据,就会导致安全漏洞。
使用RUST_BACKTRACE=1环境变量,可以显示调用栈,调用栈包含panic的位置和调用panic的函数的位置
#![allow(unused)] fn main() { $ RUST_BACKTRACE=1 cargo run thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99 stack backtrace: 0: rust_begin_unwind at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5 1: core::panicking::panic_fmt at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14 2: core::panicking::panic_bounds_check at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5 3: <usize as core::slice::index::SliceIndex<[T]>>::index at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:255:10 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:18:9 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/alloc/src/vec/mod.rs:2770:9 6: panic::main at ./src/main.rs:4:6 7: core::ops::function::FnOnce::call_once at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. }
使用调用栈需要enable debug symbol,使用cargo build/run不加--release即可
Recoverable Errors 和 Result
Result<T, E>
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {error:?}"), }; }
File::open返回一个Result<T, E>,若文件存在则返回Ok(std::fs::File)即一个文件句柄,否则返回Err(std::io::Error)
注意类似Option枚举,Result枚举同样自动预置,不必显式使用Result::
匹配不同的错误类型
use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { 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:?}"); } }, }; }
使用闭包替代匹配
use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = 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:?}"); } }); }
unwrap 和 expect
unwrap方法在Result值为OK时返回T的值,若为Err则调用panic!宏
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
类似unwrap方法,expect方法可以自定义panic!宏的错误消息
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
错误传播(Propagating)
当一个函数可能会出错时,有两种方式处理
- 函数自己处理错误
- 返回错误给调用者,让调用者选择如何处理,这被称为错误传播(propagating errors)
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), //直接返回错误 }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e),//同样返回错误 }//最后一个表达式,所有没有;也不必显式return } }
上述代码并不处理错误的逻辑,而是向上级传播错误,让调用者在合适的机会处理
使用?运算符
因为错误传播非常普遍所以提供了?运算符来简化流程
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
在Result值之后的?运算符
- 如果
Result的值为Ok,则表达式返回Ok中的值,程序继续执行 - 如果
Result的值为Err,Err就像使用return一样直接从整个函数返回。这样错误也就传播给调用者了
?运算符不同于match的点是,若Err中类型实现了From trait,则?运算符会自动调用From trait的from方法来转换类型。当函数定义一种类型去处理所有可能类型的错误时很实用
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); //链式调用搭配 ? 大幅简化代码 File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } }
因为从文件中读取字符串是相当常见的操作,所以std::fs模块提供了read_to_string函数
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
何处可用?运算符
只有函数的返回类型和?运算符使用的值一致时才可用
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt")?; } //显然main方法的返回值不是Result<T, E> //error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
从错误输出中可发现
ResultOption- 以及实现了
FromResidual的类型
可以用?运算符处理
类似Result上使用?运算符,Option的?运算符逻辑为
- 如果
Option的值为None,则函数直接返回None - 如果
Option的值为Some,则表达式返回Some中的值,函数继续执行
#![allow(unused)] fn main() { fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } }
虽然main通常返回空元组unit,但也可以返回Result<(), E>
use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; //返回类型一致,?运算符就可用了 Ok(())//返回空元组 }
当main方法返回Result<(), E>时
- 如果
main方法返回Ok(()),则程序结束值为0 - 如果
main方法返回Err,则程序结束值为非0值
c程序在正常结束为0,错误为非0值。rust同样兼容这个约定
To panic! or Not to panic!
例子,原型代码,测试
编写示例代码解释某些概念时,使用unwrap或其它会触发panic!的方法可表示这是一个占位符希望实际实际程序应当处理的错误
编写原型代码时,在你决定如何处理错误之前,使用unwrap和expect很方便。它们留下了清晰了标记供你完善错误处理
测试时使用panic!标记失败
错误处理指南
如果别人调用你的代码传递不合理的参数,你应当返回错误给调用者让其决定如何处理。但如果继续执行,可能会不安全或者有害,这时你应该panic!警示调用者处理其逻辑。同样如果你调用外部代码返回一个你没法处理的状态时,使用panic!是合适的
但如果错误是可预期的,应该返回Result,并在api文档中注明可能发生的错误
总结
- 选择适当的错误处理方式。
panic!或Result - 使用
?运算符让错误处理更简洁直观