rust学习十、异常处理(错误处理)以及捕获panic
在书籍中,中文译者翻译为错误,这是因为原文是"Error"。
但在很多语言中,都是书写为异常。
一、概述
rust的错误处理与众不同,前文已经提及:大家称为异常,它称为错误。
不可恢复的错误,可以大体称为panic(恐慌)! 太率性了....
那么我们的问题是:除了一些的确不可处理的异常,rust是否也和大部分语言那样使用try catch来捕获和跳过?
所谓的不可恢复错误,其实也可以被try catch处理?
为了便于称呼,考虑到panic的效果,本文把它称为致命错误!
二、用panic!宏处理错误
不可恢复错误发生的时候的通常表现:会打印出一个错误信息,展开并清理栈数据,然后退出
通过一个环境变量,可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因.
两种退出方式
- abort- 粗暴退出,让操作系统收拾残局
- 回溯栈并清理它遇到的每一个函数的数据。如果需要查看堆栈,可以把环境变量
RUST_BACKTRACE
设置为不是0即可
设置退出方式
Cargo.toml中设置,如下:
[profile.release] panic = 'abort'
上文中,panic还可以是设置为unwind
,这是默认的
上例是release,如果想在调试的时候,那么可以添加profile.dev
更多的配置,可以参考 https://www.rustwiki.org.cn/zh-CN/cargo/index.html
触发方式
- 直接调用panic!宏
- 一些可能导致不可恢复的错误,例如除以0,越绝访问向量
示例一、直接触发
输出一大堆!!!
示例二、越界访问
输出太多,截取了比较又意义的部分:已经足够明白那里发生错误的。
毫无疑问,对于第二种情况,在其它语言种,一般简单try catch就可以处理了!
三、用Result处理可恢复错误
Resutt<T,E>是什么
一个枚举类型,类似Option。
有两个成员:Ok,Err
和Option的Some,None一样,Ok,Err也是可以直接使用,不需要书写前缀,因为它们都在prelude中导入了。
注意:prelude是序章的意思,可以理解为rust为每一个应用自动导入的部分。
处理Result,处理错误类型
由于Result是类似Option的枚举(前文已经说过有Ok,Err),所以,可以这样使用:
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
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); let greeting_file2 = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
unwrap和expect的区别在于:后者会在触发错误的时候,直接打印参数值。
expect方法内部大体是这样的:
#[inline] #[track_caller] #[stable(feature = "result_expect", since = "1.4.0")] pub fn expect(self, msg: &str) -> T where E: fmt::Debug, { match self { Ok(t) => t, Err(e) => unwrap_failed(msg, &e), } }
逻辑:正常就返回值、不正常的则返回一个异常(不可恢复的错误,退出)。
3.1传播错误-如何处理错误
在大部分的语言中,有两种方式:继续抛出或者捕获之后做其它处理,例如java就是这样处理的。
这个内容比较多,所以单独一个小章节说明:
- 常规处理-根据result结果返回需要的内容
- 使用?简化处理
- 不是任何地方都可以用?
- 让main()函数可以支持?
- Box<dyn Error>
常规处理-这个没有什么好说的。
使用?简化处理
1.正常代码
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), } }
2.简化代码-1
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) }
?这里表示 match xxxx 一段内容。如果出现异常,则退出了,正常就继续:
match username_file_result { Ok(file) => file, Err(e) => return Err(e), };
因为这个过于套路,所以缩略为一个问号表示即可!
3. 继续简化的代码_2
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) }
这里主要使用链式写法简化了。毫无疑问,这个链式的还是比较受到欢迎的!!!
哪里不能用?
函数的返回值不同于?
最典型例子:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt")?; }
这是因为main的返回类型是() -- rust的什么都奇怪的很,虽然最内核大家都差不多。
让main支持?
有什么惊人之举吗?没有,就是修改返回类型。
use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; Ok(()) }
四、要不要panic!宏
原文说了一堆口水话,对于有经验的工程师而言,没有什么价值。略!
五、捕获panic
补充(2025/03/12)
如果panic设置为unwind(默认),那么还是可以捕获的,例如:
use std::panic; fn main() { catch_unwind(); //触发panic的方式之一:直接调用panic!()宏 //panic!("这是什么!"); //触发方式2:越界访问 let mut arr: Vec<i32> = vec![1, 2, 3]; arr[1] = 9; println!("{}", arr[3]); //越界访问第四个元素,触发不可恢复错误 } fn catch_unwind() { let result = panic::catch_unwind(|| { // 这里是可能会 panic 的代码 println!("Before panic"); panic!("This is a panic!"); println!("After panic"); // 这行代码不会被执行 }); match result { Ok(_) => println!("Code executed successfully."), Err(_) => println!("Code panicked and was caught."), } println!("程序继续运行-------------------------------"); }
输出如下:
看起来成功捕获了这种手动触发的问题。
如果把上面的手动触发致命错误,替换为其它的(例如除零、数据越界等),也是一样的。
不是什么都可以捕获
但是不是所有的致命错误都可以捕获,不是的,这个情况和其它语言一样的,即使你写了足够的try catch也是无用。
在rust中,不当使用原始指针就会发生这个情况,例如:
fn create_raw_pointer_use_box() { //回忆下Box指针,我们知道Box指针是一个堆上分配的智能指针。 let boxed = Box::new(5); let five = *boxed; println!("five: {}", five); let brave = Box::new(String::from("Rust")); let raw_brave = Box::into_raw(brave); unsafe{ println!("raw_brave: {:?}", *raw_brave); } //现在需要手动释放内存,否则会造成内存泄露 println!("释放raw_brave"); unsafe{ drop(Box::from_raw(raw_brave)); } println!("释放raw_brave完成"); // 以下代码需要注释掉,否则后续的代码不会执行。因为原始指针指向的内存地址是不合法的,可能会引发运行时错误。 // 很可惜,就是使用panic::catch_unwind,页无法阻止程序退出 // 虽然它会提示 thread 'main' panicked at std\src\io\stdio.rs:1123:9 unsafe{ let result = panic::catch_unwind(|| { println!("raw_brave依然存在,但这个时候它应该是一个空的: {:?}", *raw_brave); }); match result { Ok(_) => println!("一切正常"), Err(_) => println!("在访问有问题的原始指针后,程序出问题了"), } println!("--------------------- 继续运行 ----------------------- "); } }
输出如下:
可以看到,rust自己输出中有panic字眼,但是根本就无法通过catch_unwind来捕获,大概是因为这触碰到rust不可触碰的地方。
六、小结
- rust的错误处理和其它部分一样,力图做到与众不同- Error,panic!、Ok、Err、?、Box 这是凭空多出来的一些新概念(老东西换新的名称)
- 暂时没有介绍如何让所谓的不可恢复错误编程可以忽略的异常 --类似越界访问,在其它语言再正常不过了。但这个应该rust是一定有的,只是本章并没有提到。
- 利用Resut和?某种程度上,会让代码看起来比一些语言好看一些(仅仅是好看而已)
- 注意异常处理原则。 或者注意团队中的统一处理原则
- 在某些时候,致命错误(panic)也是可以捕获的,前提是项目设置panic=unwind(这是默认的)
本文来自博客园,作者:正在战斗中,转载请注明原文链接:https://www.cnblogs.com/lzfhope/p/18553226