09错误处理

编程语言常用的两种错误处理方式:

  • 异常 (例如,Java,C#)
  • 返回值(例如,Go,Rust )

Rust 将错误组合分成两个主要类别:

  • 可恢复错误(recoverable)
    • 通常代表向用户报错错误和重试操作是合理的情况,比如未找到文件。
  • 不可恢复错误(unrecoverable)
    • 通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

尝试从错误中恢复还是停止执行时的注意事项。

panic! 与不可恢复的错误

遇到 panic 时

  • 默认展开(unwinding)

    • Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。
  • 终止(abort)

    • 这会不清理数据就退出程序。操作系统会清理程序所使用的内存。

windows 下 powershell设置环境变量:

1
$env:RUST_BACKTRACE=1

Result 与可恢复的错误

Rust 希望开发者显示的处理错误,因此,可能出错的函数返回 Result 枚举类型,其定义如下:

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

Result 是比 Option 类型更为丰富的版本。它提供可能出现的错误,而不是可能缺失值。

因此 Result<T,E> 有两种可能:

  • Ok(T):发现了元素 T;
  • Err(E):发现了错误E;

Result 枚举和其成员与 Option 枚举一样,也被导入了 prelude 中,所以使用时不需要在 OkErr 之前指定 Result::

io::ErrorKind 值一个标准库提供的枚举,它的成员对应io 操作可能导致的不同错误类型。

匹配不同的错误

可以结合 match 模式匹配,来匹配并处理不同的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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),
},
};
}

失败时 panic 的简写: unwrap 和 expect

match 处理 panic 时有点冗长并且不总能很好的表明意图。

Result<T, E> 类型定义了很多辅助方法来处理各种情况。

  • unwrap ,如果Result 值是成员Okunwrap 会返回 Ok 中的值;如果 Result 是成员 Errunwrap 会为我们调用 panic!
  • expect,允许我们选择panic!的错误信息。可以更好的表明你的意图并易于追踪 panic 的根源。

区别:

  • unwrap ,使用默认的 panic! 信息;
  • expect,可以自定义panic!信息
1
2
3
4
5
6
7
8
9
use std::fs::File;

fn main() {
let f = File::open("hello.txt").unwrap(); // 调用 panic! 默认错误信息
}

fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt"); // 允许我们自定义 panic! 错误信息
}

传播错误

将错误给调用者来处理。方法返回 Err(e)

遇到错误不处理,而是直接返回给调用者。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::io;
use std::io::Read;
use std::fs::File;

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

let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e), // match 后遇到错误直接返回给调用者
};

let mut s = String::new();

match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e), // match 后遇到错误直接返回给调用者
}
}

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

Rust 提供了 ? 问号运算符,便于传播错误。

1
2
3
4
5
6
7
8
9
10
use std::io;
use std::io::Read;
use std::fs::File;

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

Result 值之后接的? 运算符,与上面章节使用 match 表达式处理完全相同。

  • 在某个函数中使用?运算符,该运算符尝试从 Result 中获取值,

    • 如果不成功,它就会接收 Error,中止函数执行,并把错误传播到调用该函数的函数。
  • 如果Result 的值是Ok,这个表达式将会返回 Ok 中的值,而程序继承执行。

    • match 处理的方式不同点,? 运算符返回的是Ok 中的值,而不是Ok本身,所以返回正确内容时,如果返回类型是 Result,则需要使用 Ok 封装一下;
  • 如果值是 ErrErr 作为整个函数的返回值,就好像只用了 return 关键字一样,将错传播给了调用者。

    • 疑问,遇到 Err,也是返回 Err 中的值?

? 运算符实现方式

总结:依赖 From trait 的 from 函数。

? 运算符将使用的错误值传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。

只要每一个错误类型都实现了 from 函数来定义如何将其他类型转换为返回自定义的错误类型,? 运算符会自动处理这些转换。

? 运算符可以使得函数的实现更简单

同时可以在 ? 运算符之后直接使用链式方法调用。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();

// 失败了返回 Err(E),符合返回类型
// 但是成功了返回展开后的结果,不符合展开后的类型,所以还需要在后面添加`Ok()`的情况
File::open("hello.txt")?.read_to_string(&mut s)?;

Ok(s)
}

注意:使用 ? 的使用,因为其与unwrap() 的返回方式类似,所以返回结果时,还需要再最后添加 Ok(T) 的返回结果。

? 运算符用于返回 Result / Option 类型的函数

如果函数返回值类型不是 Result<T,E> 或者 Option<T,E>类型(或者任何一种实现了 FromResidual trait 的类型)那么编译时会报错。

  • 只能用于…

the ? operator can only be used in a function that returns Result or Option (or another type that implements FromResidual)

try!

? 出现之前,Rust 提供了try!() 宏,两者作用相同,但是目前已经推荐使用!

不过阅读以前代码时,可能会遇到try!()

1
2
3
4
5
// 下面两者作用相同,推荐使用 `?` 符号
try!(File::open("hello.txt"))

File::open("hello.txt")?

多 error 类型

有时 Option 需要与 Result 交互,或者 Result<T, Error1> 需要与 Result<T, Error2> 交互。在这些情况下,我们希望以一种可组合且易于交互的方式管理不同的错误类型。

相互嵌入处理

方式1. 处理混合错误类型的最基本方法是将它们相互嵌入。

1
2
3
4
5
fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> {
vec.first().map(|first| {
first.parse::<i32>().map(|n| 2 * n)
})
}

有些时候,我们希望遇到错误时,停止进程(比如?),但当Option为None时继续。

可以使用形如 Result<Option<i32>, ParseIntError> 的嵌入形式。

1
2
3
4
5
6
7
fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> {
let opt = vec.first().map(|first| {
first.parse::<i32>().map(|n| 2 * n)
});

opt.map_or(Ok(None), |r| r.map(Some))
}

注意,上面使用了 map_or() 的方法,第一个参数为 default,即值为None时的情况,第二个参数才是有值时的参数,分不清时点进方法看一下方法签名。

定义error 类型

Rust 允许我们自定义 error 类型

它可以是多种错误类型的抽象

一个好的 error 类型,需要满足以下几个条件:

  • 同一个类型可以表示不同的错误?
  • 向用户显示良好的错误消息
  • 与其他类型做比较是容易的
    • 好的:Err(EmptyVec)
    • 坏的:Err("Please use a vector with at least one element".to_owned())
  • 可以保存有关错误的信息
    • 好的:Err(BadChar(c, position))
    • 坏的:Err("+ cannot be used here".to_owned())
  • 与其他错误配合的很好

例如:

1
2
3
4
5
6
// 这个自定义枚举类型 MyError 可以实现统一处理 ParseError和IOError的需求
#[derive(Debug)]
pub enum MyError {
ParseError,
IOError,
}

创建自定义错误处理器步骤:

  1. 创建一个自定义错误类型;
  2. 实现 From trait,用于把其它错误类型转化为该类型;
  3. 如果有其它需要实现的 trait,为自定义错误类型实现这些 trait;
  4. 使用自定义错误类型;

Boxing errors ???

编写简单代码同时保留原始错误的一种方法是将它们框起来。其缺点是,底层错误类型仅在运行时才知道,而不是静态确定的。

stdlib通过让Box实现从任何实现Error trait的类型到trait对象Box<Error>的转换,来帮助装箱我们的错误。通过 From trait。

??? 其实不太明白使用 Box<dyn Err> 的场景。

Wrapping errors

可以将 error 包装到自定义的 error 中。

各个包中的 Error 如何使用?

panic! 还是不 panic!

何时应该 panic! ?何时应该返回 Result

通用指导原则:

  1. 当代码对值进行操作时,应该首先验证值是有效的,并在无效时 panic!
    • 尝试操作无效数据会暴露代码BUG。例如,标准库在尝试越界访问数组时会 panic!
  2. 当可能发生有害状态的情况,建议使用 panic!,超出预期的情况;
  3. 当函数中存在预期可能出错的情况,需要考虑是否应该由调用者来处理这个错误,如果是,那么返回 Result

总结

  1. 总的来说,遇到错误,有两处处理方式:
    1. 直接 panic!,报错;
    2. 捕获 Err,然后自己处理或者传递给上层调用者;
  2. 希望程序遇到某种情况停止运行时,使用 panic!
    • 使用 panic! 时,可以直接用panic!这个宏输出;
    • 当函数返回值为 Result<T,E>时,可以使用 unwrap()expect() 方法来简化处理;
      • unwrap 方法,如果正确正常返回,如果错误则抛出 panic! 默认错误;
      • expect 方法,如果正确正常返回,如果错误,则抛出自定义的panic! 错误信息;
  3. 如果处理可能出错,也可能修复的情况,使用 Result
    • 可以自己处理;
    • 可以抛出,传给调用者处理;
      • 可以直接返回 Err,
      • 可以利用 ? 运算符简化错误传播,但函数返回值类型是有限制的
        • 查看 [Result/Option 章节](# 运算符用于返回 类型的函数);
        • ? 运算符支持链式调用;
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信