Rust 将错误组合成两个主要类别:可恢复错误recoverable)和 不可恢复错误unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。 可恢复错误 Result ,不可恢复(遇到错误时停止程序执行)错误 panic!

不可恢复错误panic!

Rust 有 panic!宏,当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。

对应 panic 时的栈展开或终止

当出现 panic 时,程序默认会开始 展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml[profile] 部分增加 panic = ‘abort’,可以由展开切换为终止。 例如,如果你想要在release模式中 panic 时直接终止
  1. [profile.release]
  2. panic = 'abort'
  1. fn main() {
  2. panic!("crash and burn");
  3. }
  4. Compiling playground v0.0.1 (/playground)
  5. Finished dev [unoptimized + debuginfo] target(s) in 0.95s
  6. Running `target/debug/playground`
  7. thread 'main' panicked at 'crash and burn', src/main.rs:2:5
  8. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

2:5 第二行第五个字符

使用 panic! 的 backtrace

  1. fn main() {
  2. let v = vec![1, 2, 3];
  3. v[99];
  4. }
  5. // error
  6. $ cargo run
  7. Compiling panic v0.1.0 (file:///projects/panic)
  8. Finished dev [unoptimized + debuginfo] target(s) in 0.27s
  9. Running `target/debug/panic`
  10. thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
  11. note: Run with `RUST_BACKTRACE=1` for a backtrace.
最后几行提醒我们可以设置 RUSTBACKTRACE 环境变量来得到一个 backtrace。backtrace_ 是一个执行到目前位置所有被调用的函数的列表。
  1. $ RUST_BACKTRACE=1 cargo run
  2. Finished dev [unoptimized + debuginfo] target(s) in 0.00s
  3. Running `target/debug/panic`
  4. thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
  5. stack backtrace:
  6. 0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
  7. at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
  8. 1: std::sys_common::backtrace::print
  9. at libstd/sys_common/backtrace.rs:71
  10. at libstd/sys_common/backtrace.rs:59
  11. 2: std::panicking::default_hook::{{closure}}
  12. at libstd/panicking.rs:211
  13. 3: std::panicking::default_hook
  14. at libstd/panicking.rs:227
  15. 4: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::get
  16. at libstd/panicking.rs:476
  17. 5: std::panicking::continue_panic_fmt
  18. at libstd/panicking.rs:390
  19. 6: std::panicking::try::do_call
  20. at libstd/panicking.rs:325
  21. 7: core::ptr::drop_in_place
  22. at libcore/panicking.rs:77
  23. 8: core::ptr::drop_in_place
  24. at libcore/panicking.rs:59
  25. 9: <usize as core::slice::SliceIndex<[T]>>::index
  26. at libcore/slice/mod.rs:2448
  27. 10: core::slice::<impl core::ops::index::Index<I> for [T]>::index
  28. at libcore/slice/mod.rs:2316
  29. 11: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
  30. at liballoc/vec.rs:1653
  31. 12: panic::main
  32. at src/main.rs:4
  33. 13: std::rt::lang_start::{{closure}}
  34. at libstd/rt.rs:74
  35. 14: std::panicking::try::do_call
  36. at libstd/rt.rs:59
  37. at libstd/panicking.rs:310
  38. 15: macho_symbol_search
  39. at libpanic_unwind/lib.rs:102
  40. 16: std::alloc::default_alloc_error_hook
  41. at libstd/panicking.rs:289
  42. at libstd/panic.rs:392
  43. at libstd/rt.rs:58
  44. 17: std::rt::lang_start
  45. at libstd/rt.rs:74
  46. 18: panic::main
阅读 backtrace 的关键是从头开始读直到发现你编写的文件,这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。 为了获取带有这些信息的 backtrace,必须启用 debug 标识。

可恢复的错误Result

  1. enum Result<T, E> {
  2. Ok(T),
  3. Err(E),
  4. }
TE 是泛型类型参数 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。 让我们调用一个返回 Result 的函数,因为它可能会失败
  1. use std::fs::File;
  2. fn main() {
  3. let f = File::open("hello.txt");
  4. let f = match f {
  5. Ok(file) => file,
  6. Err(error) => {
  7. panic!("Problem opening the file: {:?}", error)
  8. },
  9. };
  10. }
注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 OkErr 之前指定 Result:: 这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。
  1. thread 'main' panicked at 'Problem opening the file: Error { repr:
  2. Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12

匹配不同的错误

上面代码不管 File::open 是因为什么原因失败都会 panic!。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像上面代码 那样 panic!
  1. use std::fs::File;
  2. use std::io::ErrorKind;
  3. fn main() {
  4. let f = File::open("hello.txt");
  5. let f = match f {
  6. Ok(file) => file,
  7. Err(error) => match error.kind() {
  8. ErrorKind::NotFound => match File::create("hello.txt") {
  9. Ok(fc) => fc,
  10. Err(e) => panic!("Problem creating the file: {:?}", e),
  11. },
  12. // 其他错误 与前面不匹配
  13. other_error => panic!("Problem opening the file: {:?}", other_error),
  14. },
  15. };
  16. }
  17. // 较为复杂 后面会有讲解
  18. use std::fs::File;
  19. use std::io::ErrorKind;
  20. fn main() {
  21. let f = File::open("hello.txt").unwrap_or_else(|error| {
  22. if error.kind() == ErrorKind::NotFound {
  23. File::create("hello.txt").unwrap_or_else(|error| {
  24. panic!("Problem creating the file: {:?}", error);
  25. })
  26. } else {
  27. panic!("Problem opening the file: {:?}", error);
  28. }
  29. });
  30. }

失败时 panic 的简写:unwrap 和 expect

match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap,它的实现就类似于前面代码的 match 语句。如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!
  1. use std::fs::File;
  2. fn main() {
  3. let f = File::open("hello.txt").unwrap();
  4. }
  5. // 如果调用这段代码时不存在 hello.txt 文件,我们将会看到一个 unwrap 调用 panic! 时提供的错误信息:
  6. thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
  7. repr: Os { code: 2, message: "No such file or directory" } }',
  8. src/libcore/result.rs:906:4
还有另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:expect。使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。
  1. use std::fs::File;
  2. fn main() {
  3. let f = File::open("hello.txt").expect("Failed to open hello.txt");
  4. }
  5. thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
  6. 2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
expectunwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 用来调用 panic! 的错误信息将会作为参数传递给 expect ,而不像unwrap 那样使用默认的 panic! 信息。 因为这个错误信息以我们指定的文本开始,Failed to open hello.txt,将会更容易找到代码中的错误信息来自何处。如果在多处使用 unwrap,则需要花更多的时间来分析到底是哪一个 unwrap 造成了 panic,因为所有的 unwrap 调用都打印相同的信息。

传播错误

当编写一个调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理,这被称为 传播propagating)错误。这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
  1. use std::io;
  2. use std::io::Read;
  3. use std::fs::File;
  4. fn read_username_from_file() -> Result<String, io::Error> {
  5. let f = File::open("hello.txt");
  6. let mut f = match f {
  7. Ok(file) => file,
  8. Err(e) => return Err(e),
  9. };
  10. let mut s = String::new();
  11. // 隐形return
  12. match f.read_to_string(&mut s) {
  13. Ok(_) => Ok(s),
  14. Err(e) => Err(e),
  15. }
  16. }

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

  1. use std::io;
  2. use std::io::Read;
  3. use std::fs::File;
  4. fn read_username_from_file() -> Result<String, io::Error> {
  5. let f = File::open("hello.txt");
  6. let mut f = match f {
  7. Ok(file) => file,
  8. Err(e) => return Err(e),
  9. };
  10. let mut s = String::new();
  11. // 隐形return
  12. match f.read_to_string(&mut s) {
  13. Ok(_) => Ok(s),
  14. Err(e) => Err(e),
  15. }
  16. }
  17. // 与上面代码相同功能
  18. use std::io;
  19. use std::io::Read;
  20. use std::fs::File;
  21. fn read_username_from_file() -> Result<String, io::Error> {
  22. let mut f = File::open("hello.txt")?;
  23. let mut s = String::new();
  24. f.read_to_string(&mut s)?;
  25. Ok(s)
  26. }
Result 值之后的 ? 被定义为与上面处理 Result 值的 match 表达式有着完全相同的工作方式。如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 ErrErr 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。 match 表达式与问号运算符所做的有一点不同:? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from 函数来定义如何将自身转换为返回的错误类型,? 运算符会自动处理这些转换。 ? 之后直接使用链式方法调用来进一步缩短代码
  1. use std::io;
  2. use std::io::Read;
  3. use std::fs::File;
  4. fn read_username_from_file() -> Result<String, io::Error> {
  5. let mut s = String::new();
  6. File::open("hello.txt")?.read_to_string(&mut s)?;
  7. Ok(s)
  8. }
  9. // briefly
  10. use std::io;
  11. use std::fs;
  12. fn read_username_from_file() -> Result<String, io::Error> {
  13. fs::read_to_string("hello.txt")
  14. }
Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。

? 运算符可被用于返回 Result 的函数

  1. use std::fs::File;
  2. fn main() {
  3. let f = File::open("hello.txt")?;
  4. }
  5. // error
  6. error[E0277]: the `?` operator can only be used in a function that returns
  7. `Result` or `Option` (or another type that implements `std::ops::Try`)
  8. --> src/main.rs:4:13
  9. |
  10. 4 | let f = File::open("hello.txt")?;
  11. | ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a
  12. function that returns `()`
  13. |
  14. = help: the trait `std::ops::Try` is not implemented for `()`
  15. = note: required by `std::ops::Try::from_error`
错误指出只能在返回 Result 或者其它实现了 std::ops::Try 的类型的函数中使用 ? 运算符。 当你期望在不返回 Result 的函数中调用其他返回 Result 的函数时使用 ? 的话,有两种方法修复这个问题。一种技巧是将函数返回值类型修改为 Result,如果没有其它限制阻止你这么做的话。 另一种技巧是通过合适的方法使用 matchResult 的方法之一来处理 Result
  1. use std::error::Error;
  2. use std::fs::File;
  3. fn main() -> Result<(), Box<dyn Error>> {
  4. let f = File::open("hello.txt")?;
  5. Ok(())
  6. }
Box 被称为 “trait 对象”(“trait object”) 目前可以理解 Box 为使用 ?main 允许返回的 “任何类型的错误”。

panic! 还是不 panic!

  1. use std::net::IpAddr;
  2. // parse解析成IpAddr 返回Result类型
  3. let home: IpAddr = "127.0.0.1".parse().unwrap();

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

  1. loop {
  2. // --snip--
  3. let guess: i32 = match guess.trim().parse() {
  4. Ok(num) => num,
  5. Err(_) => continue,
  6. };
  7. if guess < 1 || guess > 100 {
  8. println!("The secret number will be between 1 and 100.");
  9. continue;
  10. }
  11. match guess.cmp(&secret_number) {
  12. // --snip--
  13. }
  14. // 封装优化版
  15. pub struct Guess {
  16. value: i32,
  17. }
  18. impl Guess {
  19. pub fn new(value: i32) -> Guess {
  20. if value < 1 || value > 100 {
  21. panic!("Guess value must be between 1 and 100, got {}.", value);
  22. }
  23. Guess {
  24. value
  25. }
  26. }
  27. pub fn value(&self) -> i32 {
  28. self.value
  29. }
  30. }