rust语言入门教程pdf (rust语言学习从哪里学)

今天开始学习rust中错误处理的内容。

rust语言入门教程全集,rust深入浅出和rust编程之道

Rust中的错误可分为 可恢复错误(recoverable) 不可恢复错误(unrecoverable) 两个类别。

  • 可恢复错误通常代表向用户报告错误和重试操作是合理的情况,例如未找到文件
  • 不可恢复错误会导致程序崩溃,例如尝试访问超过数组结尾的位置

对比其他编程语言的错误处理方式:

  • Java语言采用了异常机制的方式来处理,并没有明确区分可恢复错误和不可恢复错误。(ps: 虽然Java的异常给出了Throwable, Error, Exception, RuntimeException的继承关系体系,异常分为checked exception和uncheced exceppion,但在异常传播上采用的是通过栈回溯的方式一层层传递,直到出现捕获异常的地方。 ) Java语言这种异常处理方式的优点是简化了错误处理流程,但在运行时开销比使用返回值返回错误信息的方式要大很多。
  • Go语言是明确区分可恢复错误(error)和不可恢复错误(panic)的。Go对可恢复错误采用了以函数返回错误值的形式,在函数返回时额外返回一个错误对象(error),这种方式的优点是错误处理的运行时开销小,缺点是返回的错误必须处理或者显式传播返回给上级调用,因此一个Go程序代码中会有大量的 if err!= nil {return err;}

Rust中没有异常,对于可恢复错误使用了类型 Result<T, E> ,即函数返回的错误信息通过类型系统描述。对于不可恢复错误会 panic!

1. Result<T, E>和可恢复错误

Result<T, E> 在rust中时一个枚举类型,其定义如下:

#[derive(Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
#[rustc_diagnostic_item = "result_type"]
#[stable(feature = "rust1", since = "1.0.0")]
pub enum Result<T, E> {
    /// Contains the success value
    #[lang = "Ok"]
    #[stable(feature = "rust1", since = "1.0.0")]
    Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

    /// Contains the error value
    #[lang = "Err"]
    #[stable(feature = "rust1", since = "1.0.0")]
    Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}

Result<T, E> 枚举有两个成员, Ok Err T E 是泛型参数, T 代表成功返回的 Ok 成员中的数据类型。 E 代表失败返回的 Err 成员中的错误的类型。 有了这两个泛型参数,可以将Result枚举作为函数的返回值,用于各种场景下的可恢复错误的处理,当函数成功时返回 Ok(T) ,失败时返回 Err(E)

我们注意到Result的定义上面有一个 must_use 的标注,rust的编译器会对 must_use 标注的类型做特殊处理,如果该类型对应的值没有被显式使用,则就会有一个警告。 例如下面的代码。

例1 :

fn foo() -> Result<(), String> {
    Ok(())
}

fn main() {
    foo(); // unused `std::result::Result` that must be used

}

例1的代码在调用 foo 函数时,忽略了返回值Result,因为Result上有 must_use 标注,所以Rust的编译器在编译时会报一个警告:

warning: unused `Result` that must be used
 --> src/main.rs:7:5
  |
7 |     foo(); // unused `std::result::Result` that must be used
  |     ^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: this `Result` may be an `Err` variant, which should be handled

1.1 匹配不同的错误原因

在处理错误时,很多时候需要针对不同的错误原因进行不同的处理。 下面来学习一下rust标准库中的 std::io module中是如何设计错误处理的。

std::io 中定义了一个 std::io::Result :

#[stable(feature = "rust1", since = "1.0.0")]
pub type Result<T> = result::Result<T, Error>;

io::Result 的定义可以看出, io::Result 实际上时 result::Result<T, Error> 的别名。 io::Result 中的 Err 成员类型是 io::Error

io::Error 是一个结构体,它由一个 kind() 方法签名是 pub fn kind(&self) -> ErrorKind ,返回描述错误原因枚举 ErrorKind

ErrorKind 枚举的成员是各种io错误原因,例如 NotFound , PermissionDenied

因此如果函数返回 io::Result ,失败时返回的是 io::Error 时,就可以调用kind方法,进一步匹配不同的错误原因进行不同处理。

例2 :

例2中还用到了Result的 unwrap_or_else 方法, Result<T, E> 类型定义了很多辅助方法来处理各种情况。 除了 unwrap_or_else 外,还有:

  • unwrap 方法: 如果Result的值是成员 Ok unwrap 就返回 Ok 的值;如果Result的值是成员 Err unwrap 就会调用 panic!
  • expect 方法: 与unwrap的使用方式一样,允许我们传参指定 panic! 的信息

1.2 使用?操作符传播错误

经常在编写一个函数实现时会调用另一个返回 Result<T, E> 的函数,除了在这个函数中处理错误之外,还可以选择将错误传播到上游调用者,这就是传播错误

rust还提供了强大的 ? 操作符,如果我们只想要传播错误,而不想直接处理,可以使用 ? 操作符。

例3 :

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

例3代码中第6行的 ? 操作符会被展开成类似下面的代码:

match result {
    Ok(v) => v,
    Err(e) => Err(e.into())
}

对比一下Go语言里 if err!= nil {return err;} ,在rust中传播错误是不是比较爽。

2. panic! 和不可恢复错误

rust提供了一个 panic! 宏,当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。 出现这种情况的场景通常是检测到一些类型的 bug,而且Rust程序员并不清楚该如何处理它时,对应的是不可恢复错误。

panic!表示不可恢复的错误,希望程序马上终止运行并得到崩溃信息。

可以在需要时在我们自己的代码中使用 panic! ,例如 panic!("crash and burn");

rust标准库还提供了 catch_unwind() ,可以把panic的调用栈回溯到catch_unwind的时候,作用有点类似于Go语言中的 recover

例4 :

use std::panic;
fn main() {
    let result = panic::catch_unwind(|| {
        panic!("crash");
    });
    if result.is_err() {
        println!("panic reover: {:#?}", result);
    }
    println!("exit ok!");
}

例4的代码运行结果如下:

thread 'main' panicked at 'crash', src/main.rs:4:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic reover: Err(
    Any { .. },
)
exit ok!

最后,需要注意 catch_unwind 的使用场景可对标go语言中的 recover ,不能到处滥用,因为rust的 catch_unwind 和go的 recove 一样,不是Java里的异常处理。

参考

  • https://kaisery.github.io/trpl-zh-cn/ch09-00-error-handling.html