错误处理

rust中错误主要有2类

  • recoverable error
  • unrecoverable error

对于recoverable error,如文件未找到,一般我们提醒用户,然后重试即可 unrecoverable error始终是bug的征兆,比如使用超出范围的索引访问数组,因此程序会直接终止

大多数编程语言并不区分这2种错误,而统一使用异常(Exception))机制来处理。rust没有异常而使用 Result<T, E>来处理recoverable error和当遇到unrecoverable error时使用 panic!宏来停止程序执行

Unrecoverable Errors 和 panic!宏

有两种方式可触发panic

  • 导致代码崩溃的操作(如数组访问越界)
  • 使用 panic!

Unwinding 或 Aborting

默认情况下,当发生panic时,程序会开始 unwinding,即回溯并清理堆栈,然后退出。但是回溯和清理需要大量的时间,因此rust提供立即终止的(aborting)选择,即直接终止程序但不清理

如果你需要项目打包的二进制文件尽可能小,可以选择aborting替代unwinding,在Cargo.toml的 [profile]中添加如下配置

#![allow(unused)]
fn main() {
[profile.release]
panic = 'abort'
}

panic!宏

fn main() {
    panic!("crash and burn");
}

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

这个例子输出2行,第一行为线程panic信息和触发panic的位置包括文件名和行号,第二行为panic的消息。在其它实际情况下,文件名和行号可能是调用 panic!宏的位置,而不是实际导致调用 panic!的行,可使用回溯(backtrace)找出导致panic的代码

使用panic! Backtrace

fn main() {
    let v = vec![1, 2, 3];

    //访问超过范围的索引会直接panic
    v[99];
}
//输出
thread 'main' panicked at src/main.rs:4:6:
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

在c语言中,尝试访问超过数据结构范围的索引是未定义行为(undefined behavior),或许会得到内存中与数据结构中的元素相对应的位置,即使这个内存不属于它。这种现象称为buffer overread,而且如果攻击者能够操纵索引来读取数据,就会导致安全漏洞。

使用RUST_BACKTRACE=1环境变量,可以显示调用栈,调用栈包含panic的位置和调用panic的函数的位置

#![allow(unused)]
fn main() {
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: rust_begin_unwind
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
   2: core::panicking::panic_bounds_check
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:255:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/alloc/src/vec/mod.rs:2770:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
}

使用调用栈需要enable debug symbol,使用cargo build/run不加--release即可

Recoverable Errors 和 Result

Result<T, E>

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {error:?}"),
    };
}

File::open返回一个Result<T, E>,若文件存在则返回Ok(std::fs::File)即一个文件句柄,否则返回Err(std::io::Error)

注意类似Option枚举,Result枚举同样自动预置,不必显式使用Result::

匹配不同的错误类型

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        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:?}");
            }
        },
    };
}

使用闭包替代匹配

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

unwrap 和 expect

unwrap方法在Result值为OK时返回T的值,若为Err则调用panic!

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

类似unwrap方法,expect方法可以自定义panic!宏的错误消息

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

错误传播(Propagating)

当一个函数可能会出错时,有两种方式处理

  • 函数自己处理错误
  • 返回错误给调用者,让调用者选择如何处理,这被称为错误传播(propagating errors)
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e), //直接返回错误
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),//同样返回错误
    }//最后一个表达式,所有没有;也不必显式return
}
}

上述代码并不处理错误的逻辑,而是向上级传播错误,让调用者在合适的机会处理

使用?运算符

因为错误传播非常普遍所以提供了?运算符来简化流程

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}

Result值之后的?运算符

  • 如果Result的值为Ok,则表达式返回Ok中的值,程序继续执行
  • 如果Result的值为ErrErr就像使用return一样直接从整个函数返回。这样错误也就传播给调用者了

?运算符不同于match的点是,若Err中类型实现了From trait,则?运算符会自动调用From trait的from方法来转换类型。当函数定义一种类型去处理所有可能类型的错误时很实用

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    //链式调用搭配 ? 大幅简化代码
    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}

因为从文件中读取字符串是相当常见的操作,所以std::fs模块提供了read_to_string函数

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

何处可用?运算符

只有函数的返回类型和?运算符使用的值一致时才可用

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}
//显然main方法的返回值不是Result<T, E>
//error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)

从错误输出中可发现

  • Result
  • Option
  • 以及实现了FromResidual的类型

可以用?运算符处理

类似Result上使用?运算符,Option?运算符逻辑为

  • 如果Option的值为None,则函数直接返回None
  • 如果Option的值为Some,则表达式返回Some中的值,函数继续执行
#![allow(unused)]
fn main() {
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}
}

虽然main通常返回空元组unit,但也可以返回Result<(), E>

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    //返回类型一致,?运算符就可用了

    Ok(())//返回空元组
}

main方法返回Result<(), E>

  • 如果main方法返回Ok(()),则程序结束值为0
  • 如果main方法返回Err,则程序结束值为非0值

c程序在正常结束为0,错误为非0值。rust同样兼容这个约定

To panic! or Not to panic!

例子,原型代码,测试

编写示例代码解释某些概念时,使用unwrap或其它会触发panic!的方法可表示这是一个占位符希望实际实际程序应当处理的错误

编写原型代码时,在你决定如何处理错误之前,使用unwrapexpect很方便。它们留下了清晰了标记供你完善错误处理

测试时使用panic!标记失败

错误处理指南

如果别人调用你的代码传递不合理的参数,你应当返回错误给调用者让其决定如何处理。但如果继续执行,可能会不安全或者有害,这时你应该panic!警示调用者处理其逻辑。同样如果你调用外部代码返回一个你没法处理的状态时,使用panic!是合适的

但如果错误是可预期的,应该返回Result,并在api文档中注明可能发生的错误

总结

  1. 选择适当的错误处理方式。panic!Result
  2. 使用?运算符让错误处理更简洁直观

可参考Resultpanic标准库文档