Error divided by:
- 可回复错误:常用于从系统全局来看可以接收的错误
- 不可恢复错误:该错误通常是全局性或者系统性的错误
panic!
一个场景:文件读取操作发生在系统启动阶段->一旦文件读取失败,系统启动也将失败,意味着该失败是不可恢复的,错误的原因不重要,归根结底都是不可恢复的错误
对于这些严重到影响程序运行的错误,触发panic是很好的解决方式,在rust中触发panic有2中方式:被动触发和主动调用,下面依次来看看:
被动触发
eg:
fn main(){
let v = vec![1,2,3];
v[99];
}
数组访问越界,严重的异常的错误——数组访问越界 Error:
类似的panic还有很多,而被动触发的panic是我们日常中最常遇见的,也是rust给我们的一些保护
主动调用
rust提供了panic!宏,调用执行该宏时,程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序
只有不可恢复的错误,才调用
panic!处理,只有当不知道怎么处理时,再去调用panic!
先来调用一下panic!,这里使用了最简单的代码调用,实际上最程序的任何地方都可以这么调用:
fn main(){
panic!("crash and burn");
}
Output log:
main函数所在的线程崩溃了- 使用时可以加上一个环境变量可以获取更详细的站展开信息:
- Linux/maxOs等UNIX系统:
RUST_BACKTRACE=1 cargo run - Windows(Powershell):
$env:RUST_BACKTRACE=1;cargo run
- Linux/maxOs等UNIX系统:
backrtrace 栈展开
一个真实的崩溃例子:
fn main(){
let v = vec![1,2,3];
v[99];
}
C语言中,即便越界了,依然尝试去访问,这种情况称之为缓冲区溢出,可能导致安全漏洞
宁愿 panic!,因为取到一个不属于你的值,很多时候会导致程序上的逻辑BUG!
好的,回到问题上,成功知道了问题发生的位置,但是我们想要知道该问题之前经过了哪些调用环节,按照提示用RUST_BACKTRACE=1 cargo run或者$env:RUST_BACKTRACE=1;cargo run再来运行一次程序:
上面的代码就是一次栈展开,包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方,因为咱们的main函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数时rust_begin_unwind,该函数的目的就是进行栈展开
要获取栈回溯信息,还需要开启debug标志,该标志在使用cargo run或者cargo build时自动开启(这两个操作默认是Debug运行方式)
panic是的2种终止方式
出现panic!时,程序提供了2种方式来处理终止流程:栈展开和直接终止
默认的方式是栈展开,意味着rust会回溯栈上数据和函数调用,意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于后面的复盘;直接终止,不清理数据就直接退出程序,善后工作和操作系统来负责
绝大多数用户而言,默认选择是最好的,但是当你关心最终编译出的2机制可执行文件大小时,尝试使用直接终止的方式,例如下面的配置修改Cargo.toml,实现在release模式下遇到panic直接终止:
[profile.release]
panic='abort'
线程panic后,程序是否会终止
如果是main线程,则程序会终止,其他子线程,该线程会中,但是不会影响main线程,尽量不要再main线程种做太多任务,将这些任务交给子线程做,即便子线程panic也不会导致整个程序的结束
何时该使用 panic!
之前我们讲过Result<T,E>这个枚举类型,用于表示函数的返回结果:
enum Result<T,E>{
Ok(T),
Err(E),
}
当没有错误发生时,返回一个用Result类型包裹的值OK(T),当错误时,返回一个Err(E),对于Result返回我们有很多处理方式,最简单粗暴的是unwrap和expect,这2个函数很类似,我们以unwrap举例:
use std::fmt::IpAddr;
let home:IpAddr = '127.0.0.1'.parse().unwrap();
上面的parse方法试图将字符串127.0.0.1解析为一个IP地址类型IpAddr,返回了一个Result<IpAddr,E>类型,若解析成功,则把Ok(IpAddr)中的值赋给home,若是失败,则不处理Err(E),而是直接panic
unwrap简而言之:成功则返回值,失败则panic,不进行任何错误处理。
可能导致全局有害状态
有害状态大概分为几个大类:
- 非预期的错误
- 后续代码的运行会收到显著影响
- 内存安全的问题
当错误预期发生时,返回一个错误比较合适,例如解析器接收到格式错误的数据,HTTP请求接收到错误的参数甚至该请求内的任何错误.因为数据是可预期的,因此也是可以处理的
启动时某个流程发生了错误,后续代码的运行产生了影响,那么就应该使用
panic,而不是处理错误后继续进行
panic原理剖析
调用panic!宏时,会:
- 格式化
panic信息,然后使用该信息作为参数,调用std::panic::panic_any()函数 panic_any会检查应用是否使用了panic hook,若是使用了,该hook函数就会被调用(hook是一个狗子函数,外部代码设置的,用于在panic触发时,执行外部代码所需功能)hook函数返回时,当前线程就开始进行栈展开:从panic_any开始,如果寄存器或者栈因为某些原因错乱了,那很可能该展开会发生一场,最终线程会直接停止,展开也无法继续- 展开的过程是一帧一帧的去回溯整个栈,整个帧的数据都会随之被丢弃,但是在展开的过程中,可能会遇到被用户标记为
catching的帧(通过std::panic::catch_unwind()标记),此时用户提供的catch函数就会被调用,展开就会随之停止;当然,如果catch选择在内部调用std::panic::resume_unwind()函数,则展开还会继续 还有一种情况,就是在展开过程中,如果展开本身panic了,那展开线程会终止,展开也会随之停止
一旦线程展开终止或者完成,最终的输出结果是取决于哪个线程panic:对于main线程,操作系统提供的终止功能core::intrinsics::abort()会被调用,最终结束当前的panic进程;如果是其他子进程,那么子进程就会被简单的终止,同时信息会在稍后通过std::thread::join()进行收集。
可恢复的错误 Result
有一台消息服务器,每个用户通过websocket连接到该服务器来接收和发送消息,该过程就涉及到了socket文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接panic,否则服务器会直接崩溃,所有用户都会断连,因此我们需要一种更温和的错误处理方式:Result<T,E>
enum Result<T,E>{
Ok(T),
Err(E),
}
T代表成功传入时存入的正确值的类型,存放方式是Ok(T),E代表错误时存入的错误值,存放方式是Err(E)->
use std::fs::File;
fn main(){
let f = File::open("hello.txt");
}
以上File::open返回一个Result类型,那么问题来了:
如何获知变量类型或者函数的返回类型
let f:u32 = File::open("hello.txt");
Error:
故意将f类型标记为整形,编译器立刻不同意,并且给你了正确答案:std::result::Result<std::fs::File,std::io::Error>
我靠这么长!!!,首先Result是被定义在std::result中的,而Result很常见,就被定义在了prelude中,因此无需手动引入std::result::Result,那么返回类型就被简化为Result<std::fs::File,std::io::Error>,很标准的Result<T,E>枚举定义,只不过T被替换成了具体的std::fs::File,是一个文件句柄类型,E被替换成了std::io::Error
1个返回值类型说明File::open调用成功则返回一个可以进行读写的文件句柄,若是失败,则返回一个IO错误:文件不存在或者没有访问文件的权限。总之File::open需要1个方式告知调用者成功还是失败
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)
}
}
}
对返回的错误进行处理
实际上我们可以对部分错误进行特殊处理,而不是所有的都直接panic:
use std::fs::File;
use std::io::ErrorKind;
fn main(){
let f = File::open("hello.txt");
let f = macth f{
Ok(file)=>file,
Err(error)=>macth 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 files:{:?}",other_error),
},
};
}
上面匹配出panic之后,有对error进行了详细的匹配解析
失败就panic:unwrap和expect
unwrap和expect
成功就将Ok(T)中的值取出来,失败就直接panic,能崩溃绝不BB
use std::fs::File;
fn main(){
let f = File::open("hello.txt".unwrap());
}
如果调用时候这段hello.txt不存在,那么就将unwrap直接panic
Error:
expect和unwrap相似,也是遇到错误直接panic,但是会带上自定义的错误提示信息:
use std::fs::File;
fn main(){
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
Error:
expect相比unwrap能提供更精确的错误信息
传播错误
1个设计良好的程序,一个功能设计十几层的函数调用都有可能,但是错误处理也往往不是那里调用出错,就在哪里处理,实际中,大概率会把错误层上传然后交给调用链的上有函数进行处理; eg:
use std::fs::File;
use std::io::{self,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),
}
}
Notice:
- 该函数返回一个
Result<String,io::Error>类型,读取成功是Ok(String),失败是Err(io:Error) File::open和f.read_to_string返回的是Result<T,E>中的E就是io::Error该函数将io::Error的错误层层传播,该函数的调用者会对Result<String,io::Error>进行再处理,至于怎么处理是调用者的事; 但是还是太长了,怎么简化?
传播界的大明星:?
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)
}
这里的?就是个宏,作用和上面的match一模一样
let mut f = match f{
OK(file)=>file,
Err(e)=>return Err(e),
};
结果是Ok(T),则将T赋值给f,若结果是Err(E),则返回该错误
虽然?和match功能一致,但是事实上?更胜一筹
一个设计良好的系统,肯定有自定义的错误特征,错误之间很可能存在上下级关系,例如std::io::Error和std::error::Error,前者是IO相关的错误结构体,后者是一个最最通用的标准特征,同时前者实现了后者,所以可以实现std::io::Error到std::error::Error
可以直接进行类型提升:
fn open_file()->Result<File,Box<dyn std::error::Error>>{
let mut f = File::open("hello.txt")?;
Ok(f)
}
上面代码中File::open报错返回的错误是std::io::Error类型,但是open_file函数返回的错误类型是std::error::Error的特征对象,可以看到经过?之后,变成了另一个错误类型
根本原因在于标准库中的From类型,有一个方法from,用于将一个类型转化为另一个类型,?可以自动调用该方法,然后进行隐式类型转换,因此只要函数返回的错误ReturnError实现了From<OtherError>特征,那么?就会将OtherError转化为ReturnError
强中自有强中手:
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)
}
?还是能够实现链式调用,File::open遇到错误就返回,没有错误就将OK中的值取出来用于下一个方法调用
just like:
fn read_username_from_file()->Result<String,io::Error>{
let mut s = String::new();
let mut file = match File::open("hello.txt"){
Ok(file)=>file,
Err(e)=>return Err(e),
};
match file.read_to_string(&mut s){
Ok(_)=>Ok(s),
Err(e)=>Err(e),
}
}
还有更强:
use std::fs;
use std::io;
fn read_username_from_file()->Result<String,io::Error>{
//read_to_string是定义再std::io中的方法,因此需要在上面进行引用
fs::read_to_string("hello.txt")
}
…作者给我杆蒙了
?用于Option的返回
?还可以用于Result的传播,还能用于Option的传播,
pub enum Option<T>{
Some(T),
None
}
Result通过?返回错误,那么Option就通过?返回None:
fn first(arr:&[i32])->Option<&i32>{
let v = arr.get(0)?;
Some(v)
}
更简单的:
fn first(arr:&[i32])->Option<&i32>{
arr.get(0)
}
…
fn last_char_of_first_line(text:&str)->Option<char>{
text.line().next?.chars().last()
}
1个链式调用的例子
新手用?会犯的错误
初学者在使用?时,总是会犯错,例如:
fn first(arr:&[i32])->Option<&i32>{
arr.get(0)?
}
No;
?需要一个变量来承载正确的值,这个函数只会返回Some(&i32)或者None,只有错误值能直接返回,正确的值不行,若存在0号元素,那么函数第2行使用?后的返回类型为&i32而不是Some(&i32),智能用于:
let v = xxx()?;xxx()?.yyy()?;
带返回值的main函数
了解了?的使用限制,很容易看出错误
use std::fs::File;
fn main(){
let f = File::open("hello.txt")?;
}
Error:
因为?要求Result<T,E>形式的返回值,而main函数的返回值是(),无法满足
事实上rust还支持这种形式的main函数:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(),Box<dyn Error>>{
let f = File::open("hello.txt")?;
Ok(())
}
使用了?就可以提前返回了,通过我们又一次看见了Box<dyn Error>特征对象,因为std::error::Error是rust中层次最高的error,其他标准库中的错误都实现该错误,因此我们可以用该特征对象代表一切错误,就算main函数中调用任何标准库函数有错误,都可以通过Box<dyn Error>返回
至于main函数可以实现多种返回值,是因为实现了std::process::Termination特征…但是还没有被收录到稳定版rust中
try!
?横空出世之前,rust开发者还使用try!来处理错误
macro_rules! try{
($e:expr)=>(match $e{
Ok(val)=>val,
Err(err)=>return Err(::std::convert::From::from(err)),
});
}
和?对比:
let x = function_with_erroe()?;
let x = try!(function_with_error());
try!天已死,?天当立