本文作为对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、运行中发生
panicfn 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:352、手动触发
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:13panic时的栈展开或终止 (是否清理栈中的函数)当出现
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天