温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Rust错误处理有哪些

发布时间:2021-10-12 15:46:20 来源:亿速云 阅读:186 作者:iii 栏目:编程语言

这篇文章主要介绍“Rust错误处理有哪些”,在日常操作中,相信很多人在Rust错误处理有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Rust错误处理有哪些”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

Rust错误处理有哪些

错误处理是编程语言中很重要的一个方面。目前,错误处理的方式分为两类,第一类是以C语言为首的基于返回值的错误处理方案,第二类是以Java语言为首的基于异常的错误处理方案。也可以从发生了错误是否可恢复来进行分类,例如,C语言中对可恢复的错误会使用错误码返回值,对不可恢复的错误会直接调用exit来退出程序;Java的异常体系分为ExceptionError,分别对应可恢复错误和不可恢复错误。在Rust中,错误处理的方案和C语言类似,但更加完善好用:对于不可恢复错误,使用panic来处理,使得程序直接退出并可输出相关信息;对于可恢复错误,使用OptionResult来对返回值进行封装,表达能力更强。

不可恢复错误

panic简介

对于不可恢复错误,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:
   函数调用栈...
panic实现机制

在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>的典型用法,Iteratornext方法要么返回下一个元素,要么无元素可返回,因此使用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,则调用Fromtrait做类型转换,然后中断当前逻辑提前返回。

需要注意的是,问号运算符的引入给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错误处理有哪些”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI