Rust 错误处理

本文略有删减,原文请访问错误处理

panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。
Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败,可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。

用 panic! 处理不可恢复的错误

在实践中有两种方法造成 panic:执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者显式调用 panic! 宏。

对应 panic 时的栈展开或终止

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

如果需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止,如在 release 模式中 panic 时直接终止:

[profile.release]
panic = 'abort'

在一个简单的程序中调用 panic!:

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

使用 panic! 的 backtrace

让我们来看看另一个因为我们代码中的 bug 引起的别的库中 panic! 的例子,而不是直接的宏调用。
尝试访问超越 vector 结尾的元素,这会造成 panic!:

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

    v[99];
}

可以设置 RUST_BACKTRACE 环境变量来得到一个 backtrace,backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。

将 RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取 backtrace 看看:

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
   2: core::panicking::panic_bounds_check
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
   6: panic::main
             at ./src/main.rs:4:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样。

Windows设置 RUST_BACKTRACE 环境变量的两种方式

在cmd中执行

set RUST_BACKTRACE=1

在powershell中执行:

$env:RUST_BACKTRACE=1 ; cargo run

用 Result 处理可恢复的错误

使用 Result 类型来处理潜在的错误中的那个 Result 枚举,它定义有Ok 和 Err两个成员:

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

T 和 E 是泛型类型参数:

  • T 代表成功时返回的 Ok 成员中的数据的类型
  • E 代表失败时返回的 Err 成员中的错误的类型

调用一个返回 Result 的函数,它打开一个文件,可能会失败:

use std::fs::File;

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

File::open 的返回值是 Result<T, E>:

  • 泛型参数 T 会被 File::open 的实现放入成功返回值的类型 std::fs::File,这是一个文件句柄。
  • 错误返回值使用的 E 的类型是 std::io::Error

`File::open`` 调用可能成功并返回一个可以读写的文件句柄,也可能会失败,如文件不存在或无访问权限。

需要在代码中增加根据 File::open 返回值进行不同处理的逻辑:

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),
    };
}

如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时 panic! 宏的输出能告诉了我们出错的地方。

匹配不同的错误

使用不同的方式处理不同类型的错误:

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() {
            //尝试打开的文件并不存在,通过 File::create 创建文件
            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);
            }
        },
    };
}
  • File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。
  • io::Error 结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。
  • io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。

不同于使用 match 和 Result<T, E>

在处理代码中的 Result<T, E> 值时,相比于使用 match ,使用闭包和 unwrap_or_else 方法会更加简洁:

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);
        }
    });
}

失败时 panic 的简写:unwrap 和 expect

Result<T, E> 类型定义了很多辅助方法来处理各种情况,其中之一叫做 unwrap:

  • 如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。
  • 如果 Result 是成员 Err,unwrap 会为我们调用 panic!。

一个实践 unwrap 的例子:

use std::fs::File;

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

另一个类似于 unwrap 的方法它还允许选择 panic! 的错误信息:expect,语法如下:

use std::fs::File;

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

expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。

在生产级别的代码中,大部分人选择 expect 而不是 unwrap 并提供更多关于为何操作期望是一直成功的上下文。

传播错误

一个从文件中读取用户名的函数,如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

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 语句
    }
}

函数的返回值类型为 Result<String, io::Error>,说明函数返回一个 Result<T, E> 类型的值:泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error。

这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open 函数和 read_to_string 方法。

传播错误的简写:? 运算符

一个使用 ? 运算符向调用者返回错误的函数:

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 值的 match 表达式有着完全相同的工作方式。不同的是,? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。

问号运算符之后的链式方法调用:

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)
}

使用 fs::read_to_string 而不是打开后读取文件

use std::fs;
use std::io;

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

哪里可以使用 ? 运算符

? 运算符只能被用于返回值与 ? 作用的值相兼容的函数,因为 ? 运算符被定义为从函数中提早返回一个值,函数的返回值必须是 Result 才能与这个 return 相兼容。

尝试在返回 () 的 main 函数中使用 ? 的代码不能编译:

use std::fs::File;

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

当编译这些代码时会出错,错误指出只能在返回 Result 或者其它实现了 FromResidual 的类型的函数中使用 ? 运算符

为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为 Result<T, E>。另一个是使用 match 或 Result<T, E> 的方法中合适的一个来处理 Result<T, E>。

在 Option<T> 上调用 ? 运算符的行为与 Result<T, E> 类似:如果值是 None,此时 None 会从函数中提前返回。如果值是 Some,Some 中的值作为表达式的返回值同时函数继续

在 Option 值上使用 ? 运算符:

//从给定文本中返回第一行最后一个字符
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

注意你可以在返回 Result 的函数中对 Result 使用 ? 运算符,可以在返回 Option 的函数中对 Option 使用 ? 运算符,但是不可以混合搭配。? 运算符不会自动将 Result 转化为 Option,反之亦然;在这些情况下,可以使用类似 Result 的 ok 方法或者 Option 的 ok_or 方法来显式转换。

修改 main 返回 Result<(), E> 允许对 Result 值使用 ? 运算符:

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

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

目前可以将 Box<dyn Error> 理解为 “任何类型的错误”,在返回 Box<dyn Error> 错误类型 main 函数中对 Result 使用 ? 是允许的,因为它允许任何 Err 值提前返回。

要不要 panic!

示例、代码原型和测试都非常适合 panic

调用一个类似 unwrap 这样可能 panic! 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。
在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。

当我们比编译器知道更多的情况

当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 或者 expect 也是合适的,虽然编译器无法理解这种逻辑:

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"
    .parse()
    .expect("Hardcoded IP address should be valid");

可以看出 127.0.0.1 是一个有效的 IP 地址,所以这里使用 expect 是可以接受的。

错误处理指导原则

在当有可能会导致有害状态的情况下建议使用 panic!,有害状态是指当一些假设、保证、协议或不可变性被打破的状态(如无效的值、自相矛盾的值或者被传递了不存在的值),外加如下几种情况:

  • 有害状态是非预期的行为,与偶尔会发生的行为相对,比如用户输入了错误格式的数据。
  • 在此之后代码的运行依赖于不处于这种有害状态,而不是在每一步都检查是否有问题。
  • 没有可行的手段来将有害状态信息编码进所使用的类型中的情况。

当接收到无效输入时,优先返回错误信息以通知用户。若继续执行可能引发安全问题或严重后果,则调用 panic! 停止程序并指出bug。同样,在遇到无法修复的外部代码无效状态时,适宜使用 panic!。

当错误属于预期范围(如解析错误或HTTP限流响应),应返回 Result,表明可能出现失败,并将问题传递给调用者处理。此时不宜使用 panic! 来应对这些情况。

当代码执行操作可能因无效值而危及安全时,应先验证其有效性,并在无效时 panic!。这是因为尝试处理此类数据易暴露漏洞,如数组越界访问会导致 panic! 以防止潜在的安全风险。函数遵循输入条件的契约,若违反契约则通过 panic! 指出调用方 bug,因为这种错误通常无法在函数内部妥善处理,需要程序员修复源代码。函数契约及其可能导致 panic! 的情况应在 API 文档中明确说明。

虽然大量错误检查可能冗长繁琐,但 Rust 的类型系统和编译器能帮你自动进行许多检查。若函数参数为非 Option 类型,编译器确保其非空且有有效值,因此无需在代码中处理 Some/None 情况。传递空值的代码无法通过编译,故函数运行时无须判空。同样,使用如 u32 的无符号整型可确保值永不会为负数。

创建自定义类型进行有效性验证

使用 if 表达式检查值是否超出范围(代码冗余、可能影响性能):

loop {
    // --snip--
    let guess: i32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };
    if guess < 1 || guess > 100 {
        println!("The secret number will be between 1 and 100.");
        continue;
    }
    match guess.cmp(&secret_number) {
        // --snip--
}

可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全地在函数签名中使用新类型并相信它们接收到的值。

一个 Guess 类型,它只在值位于 1 和 100 之间时才继续:

pub struct Guess {
    //私有的字段,确保了不会存在没有通过 Guess::new 函数的条件检查的 value 
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }

    //有时被称为 getter,目的就是返回对应字段的数据
    pub fn value(&self) -> i32 {
        self.value
    }
}

热门相关:惹火甜妻:老公大人,宠上瘾!   官策   官策   随身英雄杀   陆先生偏要以婚相许