【Rust基础】Rust学习笔记 - 第6天

本文作为对Rust的基础学习,建议每一位对Rust感兴趣并且想要接触Rust的同学,将本文中所有例子都自己运行一遍,不要求你手动重写,至少将例子复制下来运行查看效果。

本文基于:

rust: 1.67.0 (fc594f156 2023-01-24)

cargo: 1.67.0 (8ecd4f20a 2023-01-10)

参考文档:Rust 程序设计语言 简体中文版

  • Rust中的程序错误panic(程序恐慌)

    在编程中,一旦程序出现错误,这时候就需要处理对应的错误,Rust则提供了一个panic的概念(Java中称为异常)。

    对于Rust而言panic只会出现在程序运行中,通常是以下两种触发方式:

    程序在运行时自己出现意外

    手动触发程序的panic

    分别模拟一下两种panic情况:

    1、运行中发生panic

    fn main() {
        println!("程序运行!");
    
        let arr = [1, 3, 5, 7, 8];
    
        for i in 0..10 {
            println!("arr[{i}] = {}", arr[i]);
        }
    
        println!("程序结束!");
    }
    

    上例,for中将会引起程序panic,因为它会尝试访问数组下标为5的内存区域;然而,该内存区域并不属于arr数组,因此会发生:index out of bounds也就是常见的数组下标越界。

    程序运行结果:

    程序运行!
    arr[0] = 1
    arr[1] = 3
    arr[2] = 5
    arr[3] = 7
    arr[4] = 8
    thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src\main.rs:7:35
    

    2、手动触发panic

    编程中可能会有手动触发panic的情况,举例一个业务场景:

    某个api接口一天只允许某个用户访问10次,一旦超过十次就禁止访问
    (只是举个例子,实现这个功能不一定要程序panic,因为panic的代价很大,如果不妥善处理,程序将会直接停止)

    fn main() {
        println!("程序运行!");
    
        for i in 1..=100 {
            println!("用户开始第{i}次请求..");
            if i + 1 > 10 {
                panic!("超过当日请求次数!");
            }
        }
    
        println!("程序结束!");
    }
    

    上述例子中,当i进行下一次请求(即i+1)前会判断是否超过当日请求上限,如果超过,则对程序进行panic!(),这里的panic!()是Rust中的一个,它的功能就是对程序进行panic(恐慌)操作,让程序不得不结束运行。

    程序运行结果:

    程序运行!
    用户开始第1次请求..
    用户开始第2次请求..
    用户开始第3次请求..
    用户开始第4次请求..
    用户开始第5次请求..
    用户开始第6次请求..
    用户开始第7次请求..
    用户开始第8次请求..
    用户开始第9次请求..
    用户开始第10次请求..
    thread 'main' panicked at '超过当日请求次数!', src\main.rs:7:13
    
  • panic 时的栈展开或终止 (是否清理栈中的函数)

    当出现 panic 时,程序默认会开始 展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止abort),这会不清理数据就退出程序。

    那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml[profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:

    [profile.release]
    panic = 'abort'
    

    以上是文档原文,按照理解,也就是 默认展开 时,Rust会自动处理并释放函数栈未出栈(调用结束)的函数,因此Rust会增加一部分程序体积(也就是Rust会增加一部分对函数栈的释放逻辑代码),而 终止 时Rust则不做相关处理,由操作系统自行清理。

    可以修改 Cargo.toml文件,在内部增加一个

    [profile.dev]
    panic = 'abort'
    

    然后执行 cargo run 之后,打开项目\target\debug\目录,会发现程序体积会有缩小的情况。

  • backtrace

    可以打印出引发panic的函数栈信息,待测试,可参考:【使用 panic! 的 backtrace

  • 处理程序引发的panic

    文档中举例了一个打开文件的例子File::open

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt");
    }
    

    返回的 f 不可变变量是一个 Result 的枚举结构,其中包含两个枚举字段:

    enum Result<T, E> {
        Ok(T),
        Err(E),
    }
    

    顾名思义,一个是成功,一个是失败;因为程序运行中谁也不能保证hello.txt文件存在。

    而这种返回的枚举结构则可以通过先前的 match 进行模式匹配:

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt");
    
        let file = match f {
            Ok(file) => file,
            Err(e) => panic!("文件打开失败: {:?}", e),
        };
    
        println!("文件打开成功:{:?}", file);
    }
    

    同样的,文件的打开错误可能分为很多种,比如:文件不存在;文件被另外一个进程独占导致无法读写等。

    Err的笼统处理不会对这几类错误进行区分,我们可以通过Err.kind()函数对其进行判断:

    use std::{fs::File, io::ErrorKind};
    
    fn main() {
        let f = File::open("hello.txt");
    
        let file = match f {
            Ok(file) => file,
            Err(error) => match error.kind() {
                /* ErrorKind::NotFound => {
                    panic!("文件不存在!");
                } */
                ErrorKind::NotFound => match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!("文件创建失败: {:?}", e),
                },
                ohter => {
                    panic!("文件打开失败: {:?}", ohter);
                }
            },
        };
        
        println!("文件打开成功:{:?}", file);
    }
    

    上例中,当文件 hello.txt 不存在(NotFound)时,则创建一个hello.txt文件,并返回它的句柄。

  • 使用unwrap()函数来简化Result.OK

    上例的书写虽然可以较好的处理文件打开的错误逻辑,但仅仅是一个打开文件就已经写了十余行代码,Rust提供了一个unwarp来简化处理逻辑:

    fn main() {
        let file = File::open("hello.txt").unwrap();
        println!("文件打开成功:{:?}", file);
    }
    

    上述代码f.unwrap()会尝试返回 Result.OK 的内容,而如果结果为Result.Error则将它会为程序触发panic! 就不需要我们手动的再去书写 match匹配。

    同样的,Rust还提供了一个expect()函数,它的功能与unwarp()类似,这个函数在第1天的猜数字游戏中已经见过:

    use std::fs::File;
    
    fn main() {
        let file = File::open("hello.txt").expect("文件打开失败!`");
        println!("文件打开成功:{:?}", file);
    }
    
  • Rust中的错误传递(抛出)

    错误传递,我更习惯称之为:错误抛出。

    首先解释:为什么需要错误抛出?

    我直接在程序中将所有的错误都处理了,错误抛出不就是多此一举了?

    答案是:对于一个完整的,能够正常运行的程序,自然是将所有的错误都处理了,否则就叫程序Bug;然而对于一个不完整的,或者说是一个库/模块来说,错误的抛出用处可就非常大了。

    use std::{
        fs::File,
        io::{self, Read},
    };
    
    fn main() {
        let r = read_text("hello.txt");
        match r {
            Ok(text) => println!("{}", text),
            Err(e) => panic!("读取失败: {}", e),
        }
    }
    
    fn read_text(filename: &str) -> Result<String, io::Error> {
        match File::open(filename) {
            Ok(mut file) => {
                let mut text = String::new();
                match file.read_to_string(&mut text) {
                    Ok(_) => Ok(text),
                    Err(e) => Err(e),
                } //没有分号哦, 注意这里的返回语句
            }
            Err(e) => Err(e),
        }
    }
    
    

    上面的例子就是封装了一个读取文件内容的函数readText(),只需要传入文件名,如果该文件存在,则通过read_to_string()函数读取文件内容并返回;如果文件不存在或者内容读取错误,则返回对应的错误信息。

    这个函数的功能就是简化了文件内容的读操作。

    而对于错误的抛出,Rust提供了一个?语法糖,可以使错误的抛出更加简单,修改一下上面的代码:

    use std::{
        fs::File,
        io::{self, Read},
    };
    
    fn main() {
        let r = read_text("hello.txt");
        match r {
            Ok(text) => println!("{}", text),
            Err(e) => panic!("读取失败: {}", e),
        }
    }
    
    fn read_text(filename: &str) -> Result<String, io::Error> {
        let mut file = File::open(filename)?;
        let mut text = String::new();
        file.read_to_string(&mut text)?;
        Ok(text)
    }
    

    这里的问号你可以将它看做成一个询问逻辑:

    该条语句有panic么? 没有, 给你结果, 继续往下走吧; 有? 不好意思,你走不了!

    伪代码:

    if 发生panic {
       return Err(e);
    }
    return Ok(text);
    

    再次修改上述代码:

    use std::{
        fs::File,
        io::{self, Read},
    };
    
    fn main() {
        let r = read_text("hello.txt");
        match r {
            Ok(text) => println!("{}", text),
            Err(e) => panic!("读取失败: {}", e),
        }
    }
    
    fn read_text(filename: &str) -> Result<String, io::Error> {
        let mut text = String::new();
    
        File::open(filename)?.read_to_string(&mut text)?;
        Ok(text)
    }
    
    

    会发现,?可以链式调用!不但如此,几乎所有新出现的编程语言都有类似的写法。

  • Rust对于读取文件的更简易的写法

    use std::fs;
    
    fn main() {
        let text = fs::read_to_string("hello.txt").expect("读取失败!");
        println!("{}", text);
    }
    

    Rust的fs包已经提供了一个 read_to_string() 函数,实现直接读取某个文件的内容,它的内部实现:

    #[stable(feature = "fs_read_write", since = "1.26.0")]
    pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
        fn inner(path: &Path) -> io::Result<String> {
            let mut file = File::open(path)?;
            let mut string = String::new();
            file.read_to_string(&mut string)?;
            Ok(string)
        }
        inner(path.as_ref())
    }
    

    实际上与我们前面写的逻辑是一模一样的。

  • 关于在什么时候使用panic!()引发错误,这里就不书写了,参见:【要不要 panic!】;但是一旦引发panic,并且没有进行正确的处理,那么程序将会立即停止运行,而不会忽略错误。

时间:2023年2月8日 第6天


版权声明:本文为AoXue2017原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。