rust学习——错误处理

Rust将错误分为两大类:可恢复错误(recoverable)和不可恢复错误(unrecoverable)。 对于可恢复的错误,例如找不到文件错误,可以合理地向用户报告问题,然后重试该操作。不可恢复的错误始终是错误的症状,例如试图访问数组末尾以外的位置。

不可恢复的错误与panic!

有时,我们的代码中会发生坏事,而您对此无能为力。 在这些情况下,Rust会执行panic!宏命令。 当panic!宏执行后,程序将打印一条失败消息,展开并清理堆栈,然后退出。最常见的情况是在检测到某种错误并且程序员不清楚如何处理该错误时发生。

让我们在一个简单的程序中尝试panic!

fn main() {
    panic!("崩溃退出了");
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
     Running `target\debug\cargo_learn.exe`
thread 'main' panicked at '崩溃退出了', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\cargo_learn.exe` (exit code: 101)

panic!宏命令与js中的throw Error属于同一性质,所以相对于nodejs而言是同一个概念

使用panic!回溯
让我们借助于一个例子来看看啥时候panin!宏命令会被调用,调用不是我们的代码主动调用的是来自库的调用,当我们的代码存在错误的时候。

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

    v[99];
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target\debug\cargo_learn.exe`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src\main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\cargo_learn.exe` (exit code: 101)

在这里,我们尝试访问向量的第100个元素(索引从零开始,因此它位于索引99),但是它只有3个元素。 在这种情况下,Rust会panic。应该使用[]返回一个元素,但是如果传递无效索引,则Rust不会在此处返回正确的元素。

可恢复的错误与Result

大多数错误的严重程度还不足以要求程序完全停止。 有时,当一个函数失败时,是出于可以轻松解释和响应的原因。 例如,如果尝试打开一个文件而该操作由于该文件不存在而失败,那么可能要创建该文件而不是终止该过程。例如:

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

T和E是泛型类型参数:T表示成功情况下在Ok变量中将返回的值的类型,E表示在Err变体内的故障情况下将返回的错误的类型。 因为Result具有这些通用类型参数,所以我们可以使用Result类型和标准库在许多不同的情况下(我们要返回的成功值和错误值可能不同)定义的函数。

让我们调用一个返回Result值的函数,因为该函数可能会失败。尝试打开一个文件:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    println!("{:?}", f)
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.56s
     Running `target\debug\cargo_learn.exe`
Err(Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" })

特别上例中的f是Result类型,如果指定其他类型将会报错,比如:

use std::fs::File;

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

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
error[E0308]: mismatched types
 --> src\main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |            ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `std::result::Result`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `std::result::Result`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `cargo_learn`.

To learn more, run the command again with --verbose.

我们还可以使用match表达式来自定义对于返回值执行不同的操作:

use std::fs::File;

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

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("打开文件失败: {:?}", error),
    };
    println!("{:?}", f)
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target\debug\cargo_learn.exe`
thread 'main' panicked at '打开文件失败: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\cargo_learn.exe` (exit code: 101)

匹配不同的错误
上例中的代码会panic! 不管为什么File::open失败。 相反,我们要针对不同的失败原因采取不同的操作:如果File::open由于文件不存在而失败,我们想要创建文件并将句柄返回到新文件。 如果File::open由于其他任何原因失败(例如,由于我们没有打开文件的权限),我们仍然希望代码崩溃! 与上例相同,添加一个内部match表达式:

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!("创建文件失败: {:?}", e),
            },
            other_error => {
                panic!("打开文件失败: {:?}", other_error)
            }
        },
    };
}

错误panic的快捷方式:unwrapexpect

使用match效果很好,但是可能有点冗长,而且不一定总是能很好地传达意图。 Result 类型具有定义在其上的许多辅助方法来执行各种任务。其中一种方法称为unwrap,是一种快捷方法。如果Result值是Ok变量,则展开将在Ok内部返回值。 如果Result值是Err变体,unwrap将引起panic! 我们的宏。 这是unwrap操作的示例:

use std::fs::File;

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

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
warning: unused variable: `f`
 --> src\main.rs:4:9
  |
4 |     let f = File::open("hello.txt").unwrap();
  |         ^ help: if this is intentional, prefix it with an underscore: `_f`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target\debug\cargo_learn.exe`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:4:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\cargo_learn.exe` (exit code: 101)

另外一种方法:expect,与unwrap相似,都会导致调用panic!。使用expectunwrap相比可以很好的自定义错误信息。使用expect的方法如下:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("打开文件hello.txt出错!");
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
warning: unused variable: `f`
 --> src\main.rs:4:9
  |
4 |     let f = File::open("hello.txt").expect("打开文件hello.txt出错!");
  |         ^ help: if this is intentional, prefix it with an underscore: `_f`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.56s
     Running `target\debug\cargo_learn.exe`
thread 'main' panicked at '打开文件hello.txt出错!: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:4:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\cargo_learn.exe` (exit code: 101)

错误传递

当编写一个其实现调用可能会失败的函数时,可以将错误返回给调用代码,以便它可以决定要做什么,而不是处理该函数中的错误。这被称为错误传递,并赋予调用代码更多的控制权,那里可能有更多的信息或逻辑来规定应如何处理错误,而不是代码上下文中可用的信息或逻辑。
比如:下面这个从文件中读取特定数据的函数。如果文件不存在或无法读取,则此函数会将这些错误返回给调用此函数的代码:

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

    fn read_username_from_file() -> Result {
        let f = File::open("hello.txt");

        let mut f = match f {
            Ok(file) => file,
            Err(e) => return Err(e),
        };

        let mut s = String::new();

        match f.read_to_string(&mut s) {
            Ok(_) => Ok(s),
            Err(e) => Err(e),
        }
    }

    let test = read_username_from_file();

    println!("{:?}", test);
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.66s
     Running `target\debug\cargo_learn.exe`
Err(Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" })

此函数可以用更短的方式编写,但是我们将首先手动进行很多操作,以探讨错误处理;最后,我们将展示较短的方法。我们先来看一下函数的返回类型:Result 。这意味着函数将返回类型Result 的值,其中通用参数T已用具体类型String填充,而通用类型E已已具体类型io::Error填充。如果此函数成功执行而没有任何问题,则调用此函数的代码将收到一个包含字符串的Ok值,该字符串是该函数从文件中读取的用户名。如果此函数遇到任何问题,则调用此函数的代码将收到一个Err值,其中包含io::Error实例,该实例包含有关问题所在的更多信息。我们选择io::Error作为此函数的返回类型,因为它恰好是从我们在该函数的主体中调用的两个操作(可能会失败)返回的错误值的类型:File::open函数和read_to_string方法。

该函数的主体通过调用File::open函数开始。 然后,我们处理返回的Result值,其结果类似于清单9-4中的匹配,而不仅仅是调用panic! 在Err的情况下,我们会从此函数尽早返回,并将File::open的错误值作为该函数的错误值传递回调用代码。 如果File::open成功,我们将文件句柄存储在变量f中并继续。

然后,在变量s中创建一个新的String,并在f中的文件句柄上调用read_to_string方法,以将文件的内容读入s中。 read_to_string方法也返回结果,因为即使File::open成功,它也可能失败。 因此,我们需要另一个匹配项来处理该结果:如果read_to_string成功,则说明我们的函数已成功,并且我们从文件中返回了用户名,该文件名已由Ok包裹。如果read_to_string失败,我们将以与处理File::open的返回值的匹配中返回错误值相同的方式返回错误值。但是,我们不需要明确地说return,因为这是函数中的最后一个表达式。

然后,调用该代码的代码将处理获取包含用户名的Ok值或包含io::Error的Err值。 我们不知道调用代码将如何处理这些值。 如果调用代码获得Err值,则可能会出现panic!并崩溃程序,使用默认用户名,或从文件以外的其他位置查找用户名。 我们没有足够的信息来了解调用代码的实际作用,因此我们向上传播所有成功或错误信息,以使其能够正确处理。

传递错误的捷径:? 操作

使用?操作实现与上例完全一样的功能:

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

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

    let test = read_username_from_file();

    println!("{:?}", test);
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
    Finished dev [unoptimized + debuginfo] target(s) in 0.69s
     Running `target\debug\cargo_learn.exe`
Err(Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" })

?操作符,在定义Result值之后放置的代码,其工作方式与我们定义为处理上例中的Result值的匹配表达式几乎相同。 如果Result的值是Ok,则Ok内的值将从该表达式中返回,并且程序将继续。如果该值为Err,则将像我们使用过return关键字一样从整个函数中返回Err,以便将错误值传播到调用代码。

如此,上述示例可以更简单的表达为:

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

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

我们已将s中新String的创建移至函数的开头;该部分没有改变。我们没有创建变量f,而是将对read_to_string的调用直接链接到File::open("hello.txt")?的结果上。我们还有一个?在read_to_string调用的末尾,当File::open和read_to_string都成功而不返回错误时,我们仍然返回一个包含s中用户名的Ok值。功能再次与上述例子相同;这只是一种不同的,更符合人体工程学的编写方式。

还有一种更简单的实现方式:

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

fn read_username_from_file() -> Result {
    fs::read_to_string("hello.txt")
}

将文件读入字符串是一个相当常见的操作,因此Rust提供了便捷的fs::read_to_string函数,该函数可打开文件,创建新的String,读取文件内容,将内容放入该String中并返回。 当然,使用fs::read_to_string并不能给我们解释所有错误处理的机会,因此我们首先进行了较长的解释。

?运算符可用于返回结果的函数中

?运算符可用于返回类型为Result的函数中,因为其定义的工作方式与上例中定义的match表达式相同。 匹配的要求返回类型为Result的部分是return Err(e),因此函数的返回类型可以是Result以便与此返回兼容。
让我们看看如果使用?会发生什么? 您会记得主函数中的运算符的返回类型为():

use std::fs::File;

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

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
 --> src\main.rs:4:13
  |
3 | / fn main() {
4 | |     let f = File::open("hello.txt")?;
  | |             ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
5 | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `std::ops::Try` is not implemented for `()`
  = note: required by `std::ops::Try::from_error`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `cargo_learn`.

To learn more, run the command again with --verbose.

这个错误指出我们只允许使用?返回Result或Option或实现std::ops::Try的其他类型的函数中的运算符。 当您在不返回这些类型之一的函数中编写代码时,要使用? 当您调用其他返回Result的函数时,有两种选择可以解决此问题。 一种技术是在没有限制的情况下将函数的返回类型更改为Result。 另一种技术是使用matchResult方法之一以适当的方式处理Result

main函数是特殊的,并且其返回类型必须具有限制。 main的一种有效返回类型是(),更方便地,另一种有效返回类型是Result,如下所示:

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

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

    Ok(())
}

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
warning: unused variable: `f`
 --> src\main.rs:5:9
  |
5 |     let f = File::open("hello.txt")?;
  |         ^ help: if this is intentional, prefix it with an underscore: `_f`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.72s
     Running `target\debug\cargo_learn.exe`
Error: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }
error: process didn't exit successfully: `target\debug\cargo_learn.exe` (exit code: 1)

使用panic!还是不使用

Rust的错误处理功能旨在帮助编写更可靠的代码。 panic!宏表示程序处于无法处理的状态,可让我们告诉进程停止,而不是尝试使用无效或不正确的值进行处理。Result枚举使用Rust的类型系统来表示操作可能会以代码可以恢复的方式失败。 我们可以使用Result来告诉调用我们的代码的代码也需要处理潜在的成功或失败。panic!Result在适当的情况下将使我们的代码在遇到不可避免的问题时更加可靠。

你可能感兴趣的:(rust)