这篇文章主要介绍“Rust错误处理有哪些”,在日常操作中,相信很多人在Rust错误处理有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Rust错误处理有哪些”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
错误处理是编程语言中很重要的一个方面。目前,错误处理的方式分为两类,第一类是以C语言为首的基于返回值的错误处理方案,第二类是以Java语言为首的基于异常的错误处理方案。也可以从发生了错误是否可恢复来进行分类,例如,C语言中对可恢复的错误会使用错误码返回值,对不可恢复的错误会直接调用exit
来退出程序;Java的异常体系分为Exception
和Error
,分别对应可恢复错误和不可恢复错误。在Rust中,错误处理的方案和C语言类似,但更加完善好用:对于不可恢复错误,使用panic
来处理,使得程序直接退出并可输出相关信息;对于可恢复错误,使用Option
和Result
来对返回值进行封装,表达能力更强。
对于不可恢复错误,Rust提供了panic机制来使得程序迅速崩溃,并报告相应的出错信息。panic出现的场景一般是:如果继续执行下去就会有极其严重的内存安全问题,这种时候让程序继续执行导致的危害比崩溃更严重。举个例子:
fn main() { let v = vec![1, 2, 3]; println!("{:?}", v[6]); }
对于上面的程序,数组v
有三个元素,但索引值是6,所以运行后程序会崩溃并报以下错误:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 6', src/main.rs:176:22 stack backtrace: 函数调用栈...
在Rust中,panic的实现机制有两种方式:
unwind方式:发生panic时,会一层一层地退出函数调用栈,栈内的局部变量还可以正常析构。
abort方式:发生panic时,直接退出整个程序。
默认情况下编译器使用unwind方式,函数调用栈信息可以帮助我们快速定位发生panic的第一现场;但某些嵌入式系统因资源不足而只能选择abort方式,可以通过rustc -C panic=abort test.rs
方式指定。
在Rust中,通过unwind方式实现的panic,其内部实现方式基本与C++的异常是一样的。Rust提供了一些工具函数,可以像try-catch
机制那样让用户在代码中终止栈展开,例如:
fn main() { std::panic::catch_unwind(|| { let v = vec![1, 2, 3]; println!("{:?}", v[6]); println!("interrupted"); // 没有输出 }) .ok(); println!("continue"); // 正常输出 }
运行程序可以发现,println!("interrupted");
语句没有执行,因此在上一条语句出发了panic,这个函数调用栈开始销毁,但std::panic::catch_unwind
阻止了调用栈的继续展开,因此println!("continue");
得以正常执行。
需要注意的是,不要像try-catch
那样使用catch_unwind
来进行流程控制,Rust更推荐基于返回值的错误处理机制,因为既然发生panic了,就让程序越早崩溃越好,这有利于调试bug,而使用catch_unwind
会让错误暂时被压制,从而让错误传递到其他位置,导致不容易找到程序崩溃的第一现场。catch_unwind
主要用于以下两种情况:
在FFI的场景下,若C语言调用了Rust的函数,在Rust内部出现了panic,如果这个panic在Rust内部没处理好,直接扔到C代码中去,会导致产生“未定义行为”。
某些高级抽象机制需要阻止栈展开,例如线程池。如果一个线程中出现了panic,我们只希望把这个线程关闭,而不是将整个线程池拖下水。
对于可恢复的错误,Rust中提供了基于返回值的方案,主要基于Option<T>
和Result<T, E>
类型。Option<T>
代表返回值要么是空要么是非空,Result<T, E>
代表返回值要么是正常值的要么错误值。它们的定义如下:
pub enum Option<T> { /// No value None, /// Some value `T` Some(#[stable(feature = "rust1", since = "1.0.0")] T), } pub enum Result<T, E> { /// Contains the success value Ok(#[stable(feature = "rust1", since = "1.0.0")] T), /// Contains the error value Err(#[stable(feature = "rust1", since = "1.0.0")] E), }
我们来看一个标准库中对Result<T, E>
的典型用法,FromStr
中的from_str
方法可以通过字符串构造出当前类型的实例,但可能会构造失败。标准库中针对bool
类型实现了这个trait,正常情况返回bool
类型的值,异常情况返回ParseBoolError
类型的值:
pub trait FromStr: Sized { /// The associated error which can be returned from parsing. type Err; fn from_str(s: &str) -> Result<Self, Self::Err>; } impl FromStr for bool { type Err = ParseBoolError; fn from_str(s: &str) -> Result<bool, ParseBoolError> { match s { "true" => Ok(true), "false" => Ok(false), _ => Err(ParseBoolError { _priv: () }), } } }
我们再来看一个标准库中对Option<T>
的典型用法,Iterator
的next
方法要么返回下一个元素,要么无元素可返回,因此使用Option<T>
非常合适。
#[must_use = "iterators are lazy and do nothing unless consumed"] pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; ... }
Option<T>
类型解决了许多编程语言中存在的空指针问题。空指针这个设计在加入编程语言时没有经过深思熟虑,而只是因为易于实现而已。空指针最大的问题在于,它违背了类型系统的规定。类型规定了数据可能的取值范围,规定了在这些值上可能的操作,也规定了这些数据代表的含义,还规定了这些数据的存储方式。但是,一个普通的指针和一个空指针,哪怕它们是同样的类型,做同样的操作,所得到的结果是不同的。因此,并不能说空指针和普通指针是同一个类型,空指针在类型系统上打开了一个缺口,引入了一个必须在运行期特殊处理的值,它让编译器的类型检查在此失去了意义。对此,Rust的解决方案是把空指针null从一个值上升为一个类型,用enum类型的Option<T>
的None
来代表空指针,而Rust中的enum要求在使用时必须对enum的每一种可能性都进行处理,因此强迫程序员必须考虑到Option<T>
为None
的情形。C/C++中也增添了类似的设计,但由于前向兼容的问题,无法强制使用,因此其作用也就弱化了很多。
Rust中提供了问号运算符?
语法糖来简化Result<T, E>
和Option<T>
的使用,问号运算符的意思是,如果结果是Err
,则提前返回,否则继续执行。?
对应着std::ops::Try
这个trait,编译器会把expr?
这个表达式自动转换为以下语义:
match Try::into_result(expr) { Ok(V) => v, Err(e) => return Try::from_error(From::from(e)), }
标准库中已经为Result<T, E>
和Option<T>
两个类型实现了Try
:
impl<T> ops::Try for Option<T> { type Ok = T; type Error = NoneError; fn into_result(self) -> Result<T, NoneError> { self.ok_or(NoneError) } fn from_ok(v: T) -> Self { Some(v) } fn from_error(_: NoneError) -> Self { None } } impl<T> ops::Try for Option<T> { type Ok = T; type Error = NoneError; fn into_result(self) -> Result<T, NoneError> { self.ok_or(NoneError) } fn from_ok(v: T) -> Self { Some(v) } fn from_error(_: NoneError) -> Self { None } }
可以看到,对于Result
类型,执行问号运算符时,如果碰到Err
,则调用From
trait做类型转换,然后中断当前逻辑提前返回。
需要注意的是,问号运算符的引入给main函数带来了挑战,因为问号运算符要求函数返回值是Result
类型,而main函数是fn() -> ()
类型,解决这个问题的办法就是修改main函数的签名类型,但这样又会破坏旧代码。Rust最终的解决方案是引入了一个trait:
pub trait Termination { /// Is called to get the representation of the value as status code. /// This status code is returned to the operating system. fn report(self) -> i32; } impl Termination for () { #[inline] fn report(self) -> i32 { ExitCode::SUCCESS.report() } } impl<E: fmt::Debug> Termination for Result<(), E> { fn report(self) -> i32 { match self { Ok(()) => ().report(), Err(err) => Err::<!, _>(err).report(), } } } impl Termination for ! { fn report(self) -> i32 { self } } impl<E: fmt::Debug> Termination for Result<!, E> { fn report(self) -> i32 { let Err(err) = self; eprintln!("Error: {:?}", err); ExitCode::FAILURE.report() } } impl Termination for ExitCode { #[inline] fn report(self) -> i32 { self.0.as_i32() } }
main函数的签名就对应地改成了fn<T: Termination>() -> T
,标准库为Result类型、()
类型等都实现了这个trait,从而这些类型都可以作为main函数的返回类型了。
到此,关于“Rust错误处理有哪些”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。