我的RUST学习 ——【第九章 9-2】Result 与 可恢复的错误

本文详细介绍了Rust编程语言中处理错误的方式,包括使用Result枚举进行错误处理,match表达式的应用,以及 Panic 机制。通过示例展示了如何在遇到错误时优雅地返回错误,避免程序立即终止。此外,还讲解了?运算符的使用,简化错误处理代码,并探讨了在不同场景下何时使用Panic和返回Result。最后,强调了在编写可复用代码时错误传播的重要性。
摘要由CSDN通过智能技术生成

相比于上一节发生的严重错误,其实大多数错误并没有严重到需要程序立即停止执行。例如,因为打开一个不存在的文件失败,此时我们可以创建这个文件,而不是终止进程。

回忆一下第二章 “使用 Result 类型来处理潜在的错误” 部分中的那个 Result 枚举,它定义有如下两个成员,OkErr

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

一个简单的例子

我们在编写代码的时候,如果调用的是别人库里的函数,怎么知道返回的是不是一个 Result 呢?

use std::fs::File;

fn main () {
	let f: u32 = File::open("hello.txt");
}
error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |                  ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
  |
  = note: expected type `u32`
             found type `std::result::Result<std::fs::File, std::io::Error>`

可以看到报错的最后一行提示,发现了一个 std::result::Result<std::fs::File, std::io::Error> 类型。

这就告诉了我们,这个函数的返回值类型是什么。当执行成功时,返回值包括一个 std::fs::File 文件句柄 ,当失败时,E 的类型是 std::io::Error

因此,我们需要根据返回值处理不同的逻辑。使用第六章的 match:

use std::fs::File;

fn main () {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error);
        },
    };
}

注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 preclude 中,所以就不需要在 match 分支中的 OkErr 之前指定 Result::

这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 fmatch 之后,我们可以利用这个文件句柄来进行读写。

match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自 panic! 宏的输出:

thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:8:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\hello_cargo.exe` (exit code: 101)

匹配不同的错误

我们可以修改一下上面的代码,以适配更加复杂的情况:

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(file) => file,
                Err(e) => panic!("file not exit and created failed!"),
            },
            _ => panic!("problem opening the file"),
        },
    };
}

上面的代码虽然实现了,但是显得有一些冗余,包含了太多的match嵌套。第十三章会介绍闭包(closure)。Result<T, E> 有很多接受闭包的方法,并采用 match 表达式实现。一个更老练的 Rustacean 可能会这么写:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = 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);
        }
    });
}

虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 match 表达式且更容易阅读。在阅读完第十三章后再回到这个例子,并查看标准库文档 unwrap_or_else 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 match 表达式。

失败时 panic 的简写:unwrap 和 expect

match 能够胜任,不够显得有一些冗长。Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中一种叫做 unwrap 。如果 ResultOkunwrap 会返回 Ok 中的值。如果 ResultErrunwrap 会为我们调用 panic!

use std::fs::File;

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

如果不存在 hello.txt ,我们将会看到一个 unwrap 调用 panic!

还有一个类似于 unwrap 的方法,except 允许我们选择 panic! 的错误信息。使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。

use std::fs::File;

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

unwrap 不同的是,当返回值是 Err 时,会抛出指定的错误信息

传播错误

当编写一些供他人使用的模块、包、函数的时候,我们可以选择让调用者知道哪里出了问题,并决定如何处理。因此我们尽量不要自己处理错误,而是把错误 “传播(propagation)” 出去。

下面编写一个函数实现这一点,这个函数的返回值比较特殊—— Result<String, io::Error>

use std::io;
use std::fs::File;
use std::io::Read;
fn main () {
    let s = read_username_from_file().unwrap();
    println!("{}", s);
}
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),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) { /* 最后一行不需要return */
        Ok(_)/* 带构造的枚举好像必须加这个括号(_) */ => Ok(s),
        Err(e) => Err(e),
    }
}

调用这个函数的代码最终会得到一个包含用户名的 Ok 值,或者一个包含 io::ErrorErr 值。我们无从得知调用者会如何处理这些值。我们只需要向上传播即可,让他们自己选择合适的方法。

这种传播错误的模式在 Rust 是如此的常见,以至于 Rust 提供了 ? 问号运算符来使其更易于处理。

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

使用 ? 运算符重写上面的代码。

实例9-7
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 后的 ? 运算符是指,当 Result 的类型为 Ok 时,整个表达式会返回 Ok 中的值,并继续向下执行;如果 Result 类型为 ErrErr 中的值将作为整个函数的返回值,并结束当前上下文的执行,相当于 return

我一开始在想,如果发生异常,不应该把整个Err(error) 返回吗?下面立刻回答了我的疑问。

与之前match版本不同的是,? 运算符会将错误值传递给 from 函数,它定义于标准库的 From trait 中,用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from 函数来定义如何将自身转换为返回的错误类型,? 运算符会自动处理这些转换。 这句话也太抽象了吧?

在示例 9-7 的上下文中,File::open 调用结尾的 ? 将会把 Ok 中的值返回给变量 f。如果出现了错误,? 运算符会提早返回整个函数并将一些 Err 值传播给调用者。同理也适用于 read_to_string 调用结尾的 ?。

? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码,如示例 9-8 所示:

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

其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学(ergonomic)的写法。

说到编写这个函数的不同方法,甚至还有一个更短的写法:

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

将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数,它会自动打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。

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

? 运算符应该被用于返回值为 Result 的函数。

如果在 main 函数中使用 ? 运算符会报错,因为 main 函数的返回值是 ()

cannot use the `?` operator in a function that returns `()`

错误指出只能在返回 Result 或者其它实现了 std::ops::Try 的类型的函数中使用 ? 运算符。当你期望在不返回 Result 的函数中调用其他返回 Result 的函数时使用 ? 的话,有两种方法修复这个问题。一种技巧是将函数返回值类型修改为 Result<T, E>,如果没有其它限制阻止你这么做的话。另一种技巧是通过合适的方法使用 matchResult 的方法之一来处理 Result<T, E>

main 函数是特殊的,其必须返回什么类型是有限制的。main 函数的一个有效的返回值是 (),同时出于方便,另一个有效的返回值是 Result<T, E>,如下所示:

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

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

    Ok(())
}

Box<dyn Error> 被称为 “trait 对象”(“trait object”),第十七章 “为使用不同类型的值而设计的 trait 对象” 部分会做介绍。目前可以理解 Box<dyn Error> 为使用 ? 时 main 允许返回的 “任何类型的错误”。

现在我们讨论过了调用 panic! 或返回 Result 的细节,是时候回到他们各自适合哪些场景的话题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值