blog_post/_posts/rust的异常处理学习.md
Begild cbe11ba5e5 添加两篇博文:
1. rust的异常处理学习
2. 2024-母亲
2024-05-18 23:15:15 +08:00

11 KiB
Raw Permalink Blame History

title date tags categories index_img
rust的异常处理学习 2024-05-18 16:41:58
rust
编程
http://cdn.7niu.begild.top/rust%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E5%AD%A6%E4%B9%A0/1595915215518.png

前言

最近在学习RUST这门编程语言。 在2,3年前了解到这门语言但是也买了一本RUST编程指南2018版的但是并没有系统的学习只是翻阅了解了一下。 当时是处于对这门语言的所有权和号称的安全性感兴趣来了解的,后来不了了之。 最近下决心进行学习一大部分的原因是linux内核已经接受其作为第二编程语言说明其已经可以和C进行抗衡或者说其在一些方面相比较C有着无法比拟的优势。 C作为一个效率非常高的语言在系统级编程中是首选语言但是其指针和内存管理的灵活性导致即使是熟手也会无意中写出BUG而避免/减少这些BUG的方法往往都是良好的编程范式或者说习惯。那么RUST的优势是什么呢

经过一段时间的学习在经过所有权和异常的处理之后我觉得其在安全性或者说强制你使用更好的编程习惯方面是比C有着无可比拟的优势的 所有权我目前还不是理解的特别熟悉,并未到将所有权铭记于心的程度,还处于依照编译器的错误进行修改,也就是 “和编译器做斗争” 的阶段,但是我今天学了异常的处理我觉得真的非常值得做一篇笔记进行记录!!

下面开始:

Panic

use std::{fs::File, io::{self, ErrorKind, Read}, net::IpAddr};

fn main() {
    println!("Hello, world!");

    /* panic 属于不可恢复的错误 */
    //主动触发一个panic
    panic!("crash and burn!")
}

在panic的时候控制台会输出很多信息我们可以根据这些信息来获知崩溃的位置以及调用链。 我们可以设置一些环境变量来决定信息的多少:

set RUST_BACKTRACE=0    #关闭触发异常时的堆栈回溯。仅显示崩溃信息
set RUST_BACKTRACE=1    #打开堆栈回溯
set RUST_BACKTRACE=full #显示特别详细的堆栈回溯信息

访问其他代码触发异常 比如这里访问越界。会在vec的代码里面发生panic

    /* thread 'main' panicked at src\main.rs:9:23:
        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
    */
    let var = vec![1,2,3];
    println!("{}", var[99]);

Result的使用

  1. 在代码编写中可能会有一些异常的参数值或者状态出现在代码运行时这时候我们应该处理这些值避免进一步蔓延甚至导致panic的发生 处理的方式可能是纠错恢复也有可能是返回错误到更上层让其处理;
  2. 也有可能是调用别人提供的功能,别人返回了错误,我们需要处理这些错误。

在其他的语言中通常要么是通过一个返回值来判断,亦或是捕获异常并进行异常的处理, 但是在RUST中是没有异常的捕获处理方式的。其通过Result枚举来进行错误的承载和处理 在我们的coding过程中应该按照

  1. 如果这个函数必定不会失败。那么返回值就是根据需要的返回值类型
  2. 如果这个函数不一定会成功那么返回值就是Result枚举让上层必须处理可能存在的错误
  3. 如果函数内部可能会发生错误但是错误一旦出现不可恢复那么应该使用panic进行终止程序
//尝试打开一个文件因为文件不一定会能够打开失败所以这里返回值Result类型我们必须要进行处理
let f = File::open("hello_word.txt");
//这里使用match 表达式进行处理, 保证f一定是可用的
let mut f = match f{
    Ok(file) => file,// 成功-> 将文件对象作为match表达式的值进行返回
    Err(error) => {
        //这里根据错误的类型进行处理。
        match error.kind() {
            //如果文件不存在则尝试创建一个空文件, 并返回
            ErrorKind::NotFound => match File::create("hello_word.txt") {
                Ok(file) => file,
                Err(error) => panic!("{}", error),//创建失败错误直接panic
            }
            //其他类型错误直接panic
            _ =>  panic!("{}", error),
        }
    }
};
//如果代码能走到这里说明f一定是可用的状态
let mut buff = String::new();
let _ = f.read_to_string(&mut buff);

println!("file conext is [{}]", buff);

从上面可以看出对于可能会失败的接口采用Result的返回值可以使得调用者必须处理潜在的错误 保证结果是可用的,才能够进行后续的逻辑, 所以对于错误会强制你进行选择,该如何处理。

比如上面的例子: 对于open失败的情况根据错误的类型进行差异化的处理。直到所有的错误得到正视并处理整个open file的区块才算结束

这种方式可以使得代码的错误处理分支极其完备,减少大量我们认为不会出现的异常错误,而在发生时按照非预期的流程蔓延开来。 不过上面match不断的嵌套是一件很痛苦的处理过程

传播错误

使用Result实现传播错误的通用方法 这里演示了一种传播错误的方法我们通过将函数的返回值定义为Result的方式当我们在函数的内部实现遇到了一些问题时我们可以将问题传播给调用者使其了解问题的信息并交给他去决定该怎么处理

//定义一个函数返回值类型是Result
fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("name.txt");
    let mut f = match f {
        Ok(file) => file,
        //如果打开失败了那么就将失败的错误传播回去
        Err(error) => return Err(error),
    };
    let mut buff = String::new();
    match f.read_to_string(&mut buff) {
        //如果读取失败了则也是返回一个错误
        Err(error) => return Err(error),
        Ok(size) => {
            //如果size为0主动构造一个错误进行返回
            if size == 0 {
                return Err(io::Error::new(io::ErrorKind::Other, "File is empty"));
            }
        },
    }
    Ok(buff)
}
//对比之前的方式,下面这个函数的处理方式就是如果失败了返回到调用者使其进行
//决断确定错误的处理方式避免在函数内进行panic的情况。
match read_username_from_file() {
    Ok(name) => println!("name is {}", name),
    Err(error) => println!("err happend! {}", error),
}

? (问号) 运算符

即使是将错误进行传播也需要大量的match 表达式进行匹配并处理,并且随着函数内部功能的增加,需要处理的失败的代码大大增多。 所以RUST提供了一个专门的运算符 ? 来减少这部分match代码的代码量使得可以更多的专注在正常的代码逻辑上。 ? 运算符可以使得当错误发生时将错误进行Return。避免使用match表达式。

通过这种方式可以大大减少代码量,不过其也有限制:

  1. 这种语法必须建立在错误的类型都是一致的情况下
  2. 如果错误类型不一致或者源错误不能转化为目标错误的话是不能这样使用的,
  3. 另外?运算符号只能在Result或者option为返回值的函数才能够使用否则会编译报错。
//这是将read_username_from_file使用 ? 运算符进行简化之后的实现
fn read_username_from_file1() -> Result<String, io::Error> {
    let mut buff = String::new();
    let mut f = File::open("name.txt")?;// ? 运算符
    let size = f.read_to_string(&mut buff)?;// ? 运算符
    if size == 0 {
        return Err(io::Error::new(io::ErrorKind::Other, "File is empty"));
    }
    Ok(buff)
}

match read_username_from_file1() {
    Ok(name) => println!("name is {}", name),
    Err(error) => println!("err happend! {}", error),
}
// 其他的Result处理方式
other_panic();

链式调用

因为使用了?运算符可以保证如果函数可以走到后续的代码说明一定是成功了的,我们就可以肆无忌惮的使用这个结果, 所以可以使用链式调用来进一步缩减中间值的使用而保证一定是安全的

//使用链式调用进一步简化代码
fn read_username_from_file2() -> Result<String, io::Error> {
    let mut buff = String::new();
    if File::open("name.txt")?.read_to_string(&mut buff)? == 0 {
        return Err(io::Error::new(io::ErrorKind::Other, "File is empty"))
    }
    Ok(buff)
}

match read_username_from_file2() {
    Ok(name) => println!("name is {}", name),
    Err(error) => println!("err happend! {}", error),
}

其他处理异常的手段

除了主动的panic宏来进行错误检测后的主动panic。我们还有另外的panic的方式: unwrap 和 except

  1. unwrap可以在结果为Err或者为None的时候发生panic但是他不提供任何自定义的信息
  2. except可以附加一些信息。

那么为什么会需要这两个东西呢而不是通过match表达式检测异常?

  1. 因为开发过程中在初期阶段,我们可能没办法完整处理所有的错误,因为有些组件还没准备完成
  2. 亦或是这是一个简单程序。不需要那么健壮性,也不需要考虑各种异常。
  3. 这就是一个测试程序,目标测试的接口函数就应该不发生任何异常。

当我们在使用一些返回值是Result的函数接口的时候就可以使用这两个方法进行结果的检测和处理

fn other_panic() {
    //我们如果知道这一定是不可能会失败的那么我们可以用unwrap来进行结果的提取而不必理会
    //比如这里我们明确他是一个常量的合法的IP地址那么就直接使用unwrap来提取即可
    let ip:IpAddr = "127.0.0.1".parse().unwrap();
    //他和下面这样是一样的效果,但是我们知道不可能失败错误就没必要这么复杂了
    // let ip:IpAddr = match "127.0.0.1".parse() {
    //     Ok(ipaddr) => ipaddr,
    //     Err(error) => panic!(),
    // };

    //这是使用except的方式可以添加一些信息
    let ip:IpAddr = "127.0.0.1".parse().expect("解析ip地址失败了");
}

总结

在我看来?运算符和Result的设计简直太棒了 因为按照我的开发经历来说。一个函数最好的就是返回值固定是一个状态这个函数调用的结果对不对也就类似OK 和 ERR的枚举变体。然后需要接口返回的数据都是通过一个指针作为出参来进行数据的返回这样就可以用过函数返回值来判断是否调用成果再进行后续的处理但是这种完全取决于个人习惯 而RUST将这一机制使用Result枚举来完美实现并且配合?运算符可以更加优雅的处理!

//c
if (func(param1, param2, output) == ERR) {
    return ERR;
}
//rust
output = func(param1, param2)?;

可以看出RUST为了保证代码的安全性在对于错误的处理是及其苛刻的

  1. 不仅要求你知晓你所使用的接口函数可能会存在的错误。
  2. 而且也通过语法尽可能的鼓励你使用Result来将错误不隐藏及时传播或者处理
  3. 同时为了避免程序陷入为了处理错误而带来的大量异常处理代码,加上了?运算符大大减少了代码量。

太厉害了