• Rust中的返回值与错误处理


    返回值和错误处理

    Rust 中的错误主要分为两类:

    • 可恢复错误,通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响,Result 用于可恢复错误
    • 不可恢复错误,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的,panic! 用于不可恢复错误。

    panic! 剖析

    当遇到不可恢复错误时,Rust 为我们提供了 panic! 宏,当调用执行该宏时,程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序

    调用 panic!

    可以在程序的任何地方调用 panic!,例如:

    fn main() {
        panic!("crash and burn");
    }
    
    • 1
    • 2
    • 3

    运行后输出:

    thread 'main' panicked at 'crash and burn', src/main.rs:2:5
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    • 1
    • 2

    以上信息包含了两条重要信息:

    • main 函数所在的线程崩溃了,发生的代码位置是 src/main.rs 中的第 2 行第 5 个字符(去除该行前面的空字符)
    • 在使用时加上一个环境变量可以获取更详细的栈展开信息:
      • Linux/macOS 等 UNIX 系统: RUST_BACKTRACE=1 cargo run
      • Windows 系统(PowerShell): $env:RUST_BACKTRACE=1 ; cargo run

    栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方。因为 main 函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是 rust_begin_unwind,该函数的目的就是进行栈展开,呈现这些列表信息给我们。

    当出现 panic! 时,程序提供了两种方式来处理终止流程:栈展开直接终止。默认的方式就是 栈展开,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。

    线程panic后,如果是 main 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 main 线程。因此,尽量不要在 main 线程中做太多任务,将这些任务交由子线程去做,就算子线程 panic 也不会导致整个程序的结束。

    返回值 Result 和 ?

    File::open 需要一个方式告知调用者是成功还是失败,并同时返回具体的文件句柄(成功)或错误信息(失败),这些信息可以通过 Result 枚举提供。实际上 IO 的错误有很多种,我们需要对部分错误进行特殊处理,而不是所有错误都直接崩溃:

    use std::fs::File;
    use std::io::ErrorKind;
    
    fn main() {
        let f = File::open("hello.txt");
    
        let f = match f {
            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),
            },
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    代码很清晰,对打开文件后的 Result 类型进行匹配取值,如果是成功,则将 Ok(file) 中存放的的文件句柄 file 赋值给 f,如果失败,则将对 Err(error) 中存放的错误信息 进行详细的匹配解析:

    • 如果是文件不存在错误 ErrorKind::NotFound,就创建文件,这里创建文件File::create 也是返回 Result,因此继续用 match 对其结果进行处理:创建成功,将新的文件句柄赋值给 f,如果失败,则 panic
    • 剩下的错误,一律 panic

    失败就 panic: unwrap 和 expect

    unwrapexpect的作用是,如果返回成功,就将 Ok(T) 中的值取出来,如果失败,就直接 panic

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt").unwrap();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果调用这段代码时 hello.txt 文件不存在,那么 unwrap 就将直接 panic

    thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    • 1
    • 2

    expectunwrap 很像,也是遇到错误直接 panic, 但是会带上自定义的错误提示信息,相当于重载了错误打印的函数:

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt").expect("Failed to open hello.txt");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    报错如下:

    thread 'main' panicked at 'Failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    • 1
    • 2

    可以看出,expect 相比 unwrap 能提供更精确的错误信息,在有些场景也会更加实用。

    传播错误(?)

    程序几乎不太可能只有 A->B 形式的函数调用,一个设计良好的程序,一个功能涉及十几层的函数调用都有可能。而错误处理也往往不是哪里调用出错,就在哪里处理,实际应用中,大概率会把错误层层上传然后交给调用链的上游函数进行处理,错误传播将极为常见。

    use std::fs::File;
    use std::io;
    use std::io::Read;
    
    fn read_username_from_file() -> Result {
        let mut f = File::open("hello.txt")?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;
        Ok(s)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    有几点值得注意:

    • 该函数返回一个 Result 类型,当读取用户名成功时,返回 Ok(String),失败时,返回 Err(io:Error)
    • File::openf.read_to_string 返回的 Result 中的 E 就是 io::Error

    其实 ? 就是一个宏,它的作用跟 match 几乎一模一样:

    let mut f = match f {
        // 打开文件成功,将file句柄赋值给f
        Ok(file) => file,
        // 打开文件失败,将错误返回(向上传播)
        Err(e) => return Err(e),
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果结果是 Ok(T),则把 T 赋值给 f,如果结果是 Err(E),则返回该错误,所以 ? 特别适合用来传播错误。

    ?可以自动进行类型提升(转换):

    fn open_file() -> Result<File, Box<dyn std::error::Error>> {
        let mut f = File::open("hello.txt")?;
        Ok(f)
    }
    
    • 1
    • 2
    • 3
    • 4

    上面代码中 File::open 报错时返回的错误是 std::io::Error 类型,但是 open_file 函数返回的错误类型是 std::error::Error 的特征对象,可以看到一个错误类型通过 ? 返回后,变成了另一个错误类型,这就是 ? 的神奇之处。

    ? 还能实现链式调用,File::open 遇到错误就返回,没有错误就将 Ok 中的值取出来用于下一个方法调用:

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    还可以以更简短的代码从文件读取数据到字符串中, Rust 标准库为我们提供了 fs::read_to_string 函数,该函数内部会打开一个文件、创建 String、读取文件内容最后写入字符串并返回

    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")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    切记:? 操作符需要一个变量来承载正确的值,这个函数只会返回 Some(&i32) 或者 None,只有错误值能直接返回,正确的值不行,所以如果数组中存在 0 号元素,那么函数第二行使用 ? 后的返回类型为 &i32 而不是 Some(&i32)。因此 ? 只能用于以下形式:

    • let v = xxx()?;
    • xxx()?.yyy()?;
  • 相关阅读:
    云栖大会72小时沉浸式精彩回顾
    ubuntu20 extundelete 不能工作
    2022年湖北省住建厅特种作业操作证报名条件是什么呢?甘建二
    亚马逊云科技面向 macOS 的 Amazon 云服务器 EC2 M1 Mac 实例
    注解配置SpringMVC
    如何在el-tree懒加载并且包含下级的情况下进行数据回显-01
    el-dialog弹窗中进度条的(mqtt提供)数据无法清空(清空方法)
    NLP-信息抽取-三元组-联合抽取-多任务学习-2019:spERT【采用分类的思想实现联合抽取,实体抽取和关系抽取模型均为分类模型】
    web大作业 静态网页(地下城与勇士 10页 带视频)
    数据采集项目之业务数据(三)
  • 原文地址:https://blog.csdn.net/qq_51601649/article/details/133786451