使用 Result 的可恢复错误

多数错误都没有严重到要求程序整个地停止运行。某些时候,在某个函数失败时,必定是由于某种可易于解释进而加以响应的原因。比如在尝试打开某个文件,而因为要打开的文件不存在,那个操作失败了时,那么可能希望创建该文件,而不是中止这个进程。

回顾第二章中的 处理潜在带有 Result 类型的程序失败 小节,其中的 Result 枚举被定义为有两个变种,OkErr,如下所示:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok<T>,
    Err<E>,
}
}

这里的 TE,都属于泛型参数(generic type parameters):在第 10 章就会更深入讨论泛型。此刻需要明白的是,这里的 T 表示在操作成功情形下,那个 Ok 变种里返回值的类型,而这里的 E,则表示在失效情形下,将返回的在 Err 变种里错误的类型。由于 Result 有着这些泛型参数,因此就可以在打算返回成功值与错误值有所区别的许多不同情形下,使用到这个 Result 及定义在其上的函数。

下面就来调用一个由于其会失败,而返回 Result 值的函数。在下面清单 9-3 中,是尝试打开一个文件。

文件名:src/main.rs

use std::fs::File;

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

清单 9-3:打开某个文件

怎样知道 File::open 会返回一个 Result 呢?这里就可以看看 标准库 API 文档,或者可以询问一下编译器!在赋予 f 一个明知 不是 该函数返回值类型的类型注解,并随后尝试编译该代码时,编译器就会告知,这两个类型不匹配。给出的错误消息,就会告诉 f 的类型是什么。来试试吧!这里已知 File::open 的返回类型不是 u32,因此就把那个 let f 语句修改为下面这样:

#![allow(unused)]
fn main() {
let f: u32 = File::open("hello.txt");
}

现在尝试编译,就会给到接下来的输出:

$ cargo run                                                                                       lennyp@vm-manjaro
   Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo)
error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |            ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `Result<File, std::io::Error>`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `error_handling_demo` due to previous error

这就是说,File::open 函数的返回类型,是个 Result<T, E>。泛型参数 T,在这里已被使用成功值的类型,std::fs::File,即一个文件句柄(a file handle)填充。而用于错误值的类型 E,则为 std::io::Error

这样的返回值类型,表示到 File::open 的调用,可能会成功而返回一个能够自该处读取,或写入到该处的文件句柄。该函数调用同样可能失败:比如该文件可能不存在,或可能没有访问该文件的权限。那么这个 File::open 函数,就需要具备已知告知其是否成功或失败的方式,与此同时给到一个文件句柄,或者错误信息。这样的信息,正是这个 Result 枚举所要表达的。

此示例中,在 File::open 成功处,变量 f 中的值就会是包含了一个文件句柄的 一个 Ok 实例。而在其失败的情况下,f 中的那个值,就会是包含了有关所发生错误类别的更多信息的一个 Err 实例。

这里就需要对清单 9-3 中代码进行添加,从而根据 File::open 所返回值,而采取不同措施。下面清单 9-4 就给出了一种使用基本工具,即在第 6 章中曾讨论过的 match 表达式,对那个 Result 进行处理的方法。

文件名:src/main.rs

use std::fs::File;

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

    let f = match f {
        Ok(file) => file,
        Err(e) => panic! ("打开文件出现问题:{:?}", e),
    };
}

清单 9-4:运用 match 表达式来处理可能返回的各个 Result 变种

请注意,与 Option 枚举类似,这个 Result 枚举及其变种,是已由 Rust 前奏(the prelude)带入到作用域中了的,因此这里无需在那两个 match 支臂中的 OkErr 变种之前,指明 Result::

在返回结果为 Ok 时,此代码就会返回从 Ok 变种抽出的那个内部的 file 值,且这里随后就把那个文件句柄值,指派给那个变量 f。在这个 match 之后,就可以将这个文件句柄,用于读取或写入了。

而那个 match 的另一支臂,则处理了从 File::open 得到一个 Err 值的情形。在此示例中,选择了调用 panic! 宏。在当前目录中没有名为 hello.txt 的文件,并运行此代码时,就会看到来自那个 panic! 宏的如下输出:

$ cargo run                                                                                      lennyp@vm-manjaro
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/error_handling_demo`
thread 'main' panicked at '打开文件出现问题:Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

与往常一样,此输出告知了到底什么出错了。

匹配不同的错误

Matching on Different Errors

上面清单 9-4 中的代码,不论 File::open 因何而失败,都会 panic!。然而,这里是要因应不同失败原因,而采取不同措施:在 File::open 因为那个文件不存在而失败时,就要创建该文件并返回到那个新建文件的句柄。在那个 File::open 因别的其他原因失败 -- 比如没有打开该文件的权限时,这里仍要该代码以清单 9-4 中所做的同样方式,panic! 掉。为此,这里就要添加一个内部的 match 表达式,如下清单 9-5 中所示。

文件名:src/main.rs

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

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

    let f = match f {
        Ok(file) => file,
        Err(e) => match e.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(error) => panic! ("创建该文件时出现问题:{:?}", error),
            },
            other_error => panic! ("打开文件出现问题:{:?}", other_error),
        },
    };
}

清单 9-5:以不同方式处理不同类别的错误

File::open 所返回的位于 Err 变种内部的值的类型为 io::Error,他是一个由标准库提供的结构体。该结构体有个可供调用以获取到 io::ErrorKind 值的方法 kind。而枚举 io::ErrorKind 亦是由标准库提供,并有着表示那些可能自某个 io 操作而引起的,不同类别错误的一些变种。这里打算使用的变种为 ErrorKind::NotFound,表示了正尝试打开的文件尚不存在。因此这里既对 f 进行了匹配,而同时还有了在 e.kind() 上的一个内层匹配。

这里打算检查的那个内层匹配中的条件,则是由 e.king() 所返回的那个值,是否为 ErrorKind 枚举的 NotFound 变种。在 e.kind() 返回的值为 ErrorKindNotFound 变种时,这里就尝试以 File::create 来创建该文件。然而由于 Fiel::create 仍会失败,因此这里就需要在那个内层 match 表达式中的第二个支臂。在该文件无法被创建出来时,就会打印出一条不同的错误消息。外层那个 match 表达式的第二支臂保持原样,因此该程序会在除了文件未找到错误之外的其他任何错误时,都会中止运行。

这种结合Result<T, E> 运用 match 表达式的替代方案

那可是有好多的 matchmatch 表达式是很有用,但同样也是很原始的。在第 13 章,就会了解到闭包(closures),这种与定义在 Result<T, E> 上的众多方法一起使用的特性。在对代码中的 Result<T, E> 值进行处理时,比起使用 match 表达式,这样的闭包方式可以简练得多。 比如,下面就是编写与清单 9-5 中同样逻辑,不过却使用了闭包特性与 unwrap_or_else 方法的另一种方式。

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

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|e| {
        if e.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic! ("创建文件时发生问题:{:?}", error);
            })
        } else {
            panic! ("打开文件时出现问题:{:?}", e);
        }
    });

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

尽管此代码与清单 9-5 有着同样行为,但他并未包含任何的 match 表达式,且读起来更清楚。请在读完了第 13 章,并看看标准库文档中的这个 unwrap_or_else 方法后,再回到这个示例。在对错误进行处理时,许多别的这些方法,都可以清理掉大量嵌套的 match 表达式。

出错时而中止的快捷方式:unwrapexpect

Shortcuts for Panic on Error: unwrap and expect

运用 match 运作足够良好,不过那样可能有点冗长,且不总是良好地传达了意图。这个 Result<T, E> 类型,其上本来就定义了许多用于完成各种各样的、更为具体任务的辅助方法。其中的 unwrap 方法,就是一个实现了刚好与前面清单 9-4 中所编写的 match 表达式类似的快捷方法。在 Result 的值为 Ok 变种时,unwrap 就会返回那个 Ok 内部的值。而在该 ResultErr 变种时,unwrap 则会代为调用 panic! 宏。下面就是运作中的一个 unwrap 示例:

文件名:src/main.rs

use std::fs::File;

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

在没有 hello.txt 文件下运行此程序时,就会看到一条来自由这个 unwrap 方法做出的 panic! 宏调用的错误消息:

$ cargo run                                                         lennyp@vm-manjaro
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/error_handling_demo`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

同样,Result<T, E> 上的 expect 方法,则可实现对这条 panic! 错误消息的选取。使用 expect 而非 unwrap 并提供良好的错误消息,就能够传达到自己的意图,进而令到追踪程序中止缘由更为容易。expect 方法的语法如下所示:

文件名:src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("打开 hello.txt 失败");
}

这里以与 unwrap 同样方式,使用了 expect:用于返回文件句柄,或者对 panic! 宏进行调用。而在 expect 调用 panic! 时用到的错误消息,就将是这里传递给 expect 的那个参数,而不再是 unwrap 所用到的那个默认 panic! 消息了。下面就是该错误消息看起来的样子:

$ cargo run                                                         lennyp@vm-manjaro
   Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo)
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/error_handling_demo`
thread 'main' panicked at '打开 hello.txt 失败: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

由于此错误消息是以这里所指定的,打开 hello.txt 失败 开始,因此就会更易于搞清楚,此错误消息来自代码中的何处。而若在多处使用 unwrap,那么在要精准找出到底是那个 unwrap 导致了程序中止时,就会因为所有这些调用了 panic!unwrap,都打印出同样消息,而要耗费更多时间。

传播错误

Propagating Errors

在某函数实现调用了可能失败的某些东西时,与其在该函数自身里头对错误进行处理,还可以将该错误返回给调用该函数的代码,这样调用该函数的代码就可以自己决定要做些什么。这就叫做 传递(propagating) 错误,而将更多的控制,给到调用该函数的代码,相比于当前实现的函数代码,调用代码中可能会有更多决定该错误应如何被处理的信息或逻辑。

比如,下面清单 9-6 就给出了一个从某个文件读取用户名的函数。在那个文件不存在或无法读取时,这个函数就会将这些错误返回给调用该函数的代码。

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

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

    let mut username = String::new();

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

清单 9-6:使用 match 将错误返回给调用代码的一个函数

虽然可以简单得多的方式,来重写该函数,不过为了对错误处理进行探索,因此这里就要通过亲自动手完成其大部分代码开头;在结束时,就会给出那更简短的方式。首先来看看该函数的返回值类型:Result<String, io::Error>。这表示该函数要返回一个类型 Result<T, E> 的值,其中的泛型参数 T 已被具体类型 String 填充,而那个泛型 E 则已被具体类型 io::Error 填充。若此函数不带任何问题的成功运行,那么调用该函数的代码,就会收到一个保存着一个 StringOk 值 -- 即该函数从那个文件中读取到的用户名。而在该函数出现任何问题时,那么调用代码就会收到一个,保存着包含了有关所出现问题更多信息的 io::Error 示例的 Err 值。这里之所以选择 io::Error 作为此函数的返回值,是因为在该函数的函数体中所调用的两个都可能失败的操作:File::openread_to_string,他们所返回的错误值都是这个 io::Error 类型。

该函数的函数体,是以调用 File::open 函数开始的。随后这里就以与清单 9-4 中类似方式,使用了一个 match 处理 File::open 返回的 Result。在 File::open 成功时,那么在模式变量 file 中的文件句柄,就成为那个可变变量 f 中的值,且函数会继续执行。而在 Err 情形下,这里使用了 return 关键字,早早地就从这个函数 return 了出去,同时将来自 File::open 的那个错位值,此时是在模式变量 e 中,作为该函数的错误值,传回给调用该函数的代码。

因此在 username_file 有着一个文件句柄时,该函数随后就会创建一个在变量 username 中的新 String,并调用 username_file 中文件句柄上的 read_to_string 方法,来将该文件中的内容,读取到 username 中。因为即使 File::open 运行成功,这个 read_to_string 仍可能失败,因此他同样会返回一个 Result。那么这里就需要另一个 match,来处理这个 Result:在 read_to_string 成功时,那么接下来这个函数就成功执行了,进而就从这个文件,返回到此时位于封装在一个 Ok 中的 username 中的用户名来。而在 read_to_string 失败时,这里就会以与之前在那个处理 File::open 返回值的 match 中返回错误值的同样方式,返回现在这个 read_to_string 的错误值。不过,由于这是该函数中的最后一个表达式,因此这里无需显示地写下 return

调用此代码的代码,随后就会对收到的包含了用户名 Ok 值,或者包含了一个 io::Error 类型的 Err 值进行处理。至于要对这些值做何处理,则取决于调用代码了。在调用代码收到 Err 值时,他就可以采取好比调用 panic! 并崩溃掉该程序,可以使用某个默认用户名,或者从相比该文件的其他地方,查找该用户名等操作。这里没有关于那个调用代码确切地尝试要做什么的足够信息,因此这里就把全部的成功或错误信息,向上传递给调用代码,让调用代码进行适当处理。

由于在 Rust 中这样的传递错误模式是如此普遍,以致于 Rust 提供了问好操作符(the question mark operator, ?),来令到错误传递更加容易。

传播错误的快捷方式:? 操作符

A Shortcut for Propagating Errors: the ? Operator

下面清单 9-7 给出了与清单 9-6 有着同样功能的一个 read_username_from_file 实现,只是此实现使用了 ? 操作符。

文件名:src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

清单 9-7:一个使用 ? 操作符将错误返回给调用代码的函数

那个放在某个 Result 值后面的 ?,被定义为几乎与之前所定义的那些,用于处理清单 9-5 中那些 Result 值的 match 表达式,以同样方式运作。在 Result 的值为 Ok 时,那么那个 Ok 内部的值,就会从该表达式得以返回,且程序将继续运行。而在该 Result 值为一个 Err 时,则会如同之前曾用到的 return 关键字一样,将自这整个函数,返回这个 Err 值,进而这个错误值,就被传递给了调用代码。

清单 9-6 中的 match 表达式完成的事情,与这个 ? 操作符完成的事情有个不同点:调用了这个 ? 操作符的错误值,会经过定义在标准库中 From 特质(the From trait in the standard library)中定义的 from 函数,而该函数被用于将一种类型的值,转换到另一种类型中。当 ? 操作符调用 from 函数时,被接收到的错误类型,就被转换为了定义在当前函数返回值类型中的类型了(即 Result<String, io::Error>)。在某个函数可能失败,即便该函数的一些部分而不是整个函数,由于许多不同原因而失败,而返回一种表示这些全部失败方式的一种错误类型时,这个不同之处就会有用。

比如,这里本可将清单 9-7 中的 read_username_from_file 函数,修改为返回一个自己定义的名为 OurError 的定制错误类型。而在同时给 OurError 定义了 impl From<io::Error>,以从 io::Error 构造出一个 OurError 的实例时,那么随后无需添加任何代码到这个函数,read_username_from_file 函数中的这些 ? 操作符,就会调用 from 并对那些错误类型进行转换。

在清单 9-7 的语境下,位于 File::open 调用末尾的那个 ?,将返回一个 Ok 内部的值给变量 username_file。而在有错误发生时,这个 ? 操作符,就会早早地从整个函数退出,并把任何的 Err 值给到调用代码。对于那个 read_to_string 调用末尾处的 ?,适用这同样的情况。

? 操作符消除了很多样板代码(a lot of boilerplate),并令到此函数的实现更为简单。通过将这些方法调用在整个 ? 即刻链接起来,甚至可以进一步缩短此代码,如下清单 9-8 中所示。

文件名:src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

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

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

    Ok(username)
}
}

清单 9-8:在 ? 操作符后将方法调用链接起来

这里已将那个 username 中的新 String 的创建,挪到了该函数的开头;整个函数就整个部分未作改动。这里没有了变量 username_file 的创建,而是已将到 read_to_string 的函数调用,直接链接到了 File::open("hello.txt")? 的结果上。在 read_to_string 调用的末尾仍有一个 ?,同时在这两个 File::openread_to_string 调用都成功,而不返回错误时,这里就会返回一个包含了 usernameOk 值。功能仍旧与清单 9-6 和清单 9-7 中是一样的;这只是一种不同的、更为符合人体工程学的编写方式。

下面清单 9-9 给出了使用 fs::read_to_string 的一种甚至更加简短的方式。

文件名:src/main.rs

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

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

清单 9-9:使用 fs::read_to_string 而非打开在读取那个文件

将某个文件读取到字符串中,是个相当常见的操作,因此标准库提供了便捷的打开文件、创建一个新 String、读取文件内容、将内容放入到那个 String,并将其返回的 fs::read_to_string 函数。当然,fs::read_to_string 的使用,并不能赋予到这里对全部错误处理加以解释的机会,因此这里才要走过前面那些常常的过程。

哪些地方可以使用 ? 操作符

Where The ? Operator Can Be Used

? 操作符仅可用于那些返回值类型,与这个 ? 被用于的那个值类型兼容的函数中。这是由于 ? 操作符被定义为与在清单 9-6 中,所定义的那个 match 表达式类似方式,执行一个该函数早期阶段的退出。在清单 9-6 中,那个 match 使用的是一个 Result 值,同时那个先期返回支臂返回的是一个 Err(e) 值。那么那个函数的返回值类型,就必须是个 Result,这样才与这个 return 兼容。

在下面清单 9-10 中,就要看看一个有着与其上使用了 ? 的类型值不兼容返回值的 main 函数中,使用 ? 操作符会收到的错误:

文件名:src/mian.rs

use std::fs::File;

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

清单 9-10:尝试在返回 ()main 函数中使用 ? 就不会编译

此代码是要打开一个文件,这就可能失败。那个 ? 操作符接续了由 File::open 所返回的 Return 值,然而这个 main 函数的返回值类型为 (),而非 Result。那么在编译此代码时,就会得到以下的错误消息:

$ cargo run                                                                              lennyp@vm-manjaro
   Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | / fn main() {
4 | |     let greating_file = 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 `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error_handling_demo` due to previous error

此错误指出了只允许在返回 ResultOption 或别的实现了 FromResidual 的类型的函数中,使用 ? 操作符。

而要修正这个错误,则有两个选择。一个选择是在没有修改函数返回值类型的限制时,那么就将其修改为与在其上使用 ? 操作符的值类型兼容。另一技巧,则是使用一个 match 表达式,或某个 Result<T, E> 的那些方法,来以某种恰当方式对这个 Result<T, E> 进行处理了。

这个错误消息还提到,? 还可与 Option<T> 类型的值一同使用。与在 Result 上使用 ? 一样,可在返回一个 Option 的函数中的 Option 上使用 ?。在某个 Option<T> 上调用 ? 操作符的行为,与在 Result<T, E> 上其被调用时的行为类似:在该值为 None 时,None 就会在那个地方及早地从该函数被返回。而在该值为 Some 时,那么这个 Some 内部的值,就是该表达式的结果值,同时函数会继续执行。下面清单 9-11 有着一个在给定文本中找到第一行最后一个字符的函数示例:

#![allow(unused)]
fn main() {
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}
}

清单 9-11:在某个 Option<T> 的值上使用 ? 操作符

由于可能那里有个字符,不过同样坑能那里没有字符,因此此函数返回的是 Option<char>。这个代码取那个 text 字符串切片参数,并在其上调用了 lines 方法,该方法返回的是对该字符串中那些文本行的一个迭代器。由于此函数是要对首个文本行进行检查,因此他调用了那个迭代器上的 next,来从迭代器上获取头一个值。在 text 为空字符串时,那么这个到 next 的调用,就会返回 None,这也就是这里使用 ? 来停止这个 last_char_of_first_line 函数,并自其返回 None 的情形。而在 text 不为空字符串时,next 就会返回一个包含了在 text 中第一行文本的字符串切片的 Some 值。

此时 ? 操作符会提取这个字符串切片,进而就可以在那个字符串切片上调用 chars,来获取到他那些字符的一个迭代器。这里关心的是第一行文本中的最后一个字符,因此就要调用 last 来返回迭代器中的最后一个条目。因为首个文本行为空字符串是可能的,比如在 text 以空行开头却在其他行上有一些字符,如同在 "\nhi" 中一样,因此 last 得到一个就是个 Option 值。不过在首行上有最后一个字符时,这个字符就会在 Some 变种里被返回。中间的 ? 操作符,给到了一种表达此逻辑的简洁方式,运行在一个行里来实现该函数。若无法在 Option 上运用这个 ? 操作符,那么就必须使用更多方法调用,或 match 表达式来实现此逻辑。

注意在返回 Result 函数中的 Result 上,可以使用 ? 操作符,而在返回 Option 函数中的 Option 上,可使用 ? 操作符,但不能混用及进行匹配。? 操作符不会自动将 Result 转换为 Option,或反过来将 Option 转换为 Result;在这些情况下,是可以在 Result 上使用诸如 ok,或在 Option 上使用 ok_or 这样的方法,来显示地完成转换。

到目前为止,这里使用过的所有 main 函数,返回的都是 ()。由于 main 函数是可执行程序的进入与退出点,因此他是特殊的,而关于其返回值类型可以是什么,为了程序如预期那样执行,是有一些限制的。

幸运的是,main 函数同样可以返回 Result<(), E>。下面清单 9-12 有着来自 9-10 的代码,不过这里将 main 函数的返回值类型,改成了 Result<(), Box<dyn Error>>,并在最后添加了一个返回值 Ok(())。现在该代码就会编译了:

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

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

    Ok(())
}

清单 9-12:将 main 修改为返回 Result<(), E>,就实现了在 Result 值上 ? 操作符的使用

这里的 Box<dyn Error> 类型,是个 特质对象(trait object),在第 17 章中的 “使用允许不同类型值的特质对象” 小节,就会讲到这个特性。而现在,可将 Box<dyn Error> 理解为表示 “任何类别的错误”。由于 ? 操作符允许将任何 Err 值及早返回,因此将 ? 用在有着错误类型 Box<dyn Error>main 函数中, 某个 Result 值上是允许的。即使这个 main 函数的函数体,将只会返回类型 std::io::Error 的那些错误,而经由指定 Box<dyn Error>,即使将返回其他错误的代码添加到 main 的函数体,该函数签名 fn main() -> Result<(), Box<dyn Error>> 仍将无误。

main 函数返回了一个 Result<(), E> 时,那么若 main 返回的是 Ok(()),则该可执行程序就会以值 0 退出,并在 main 返回 Err 值时,以非零值退出。C 语言编写的可执行程序,在退出时返回的是些整数:成功退出的程序返回整数 0,而出错的程序返回某些非 0 的整数。Rust 从可执行程序返回的也是整数,从而与此约定兼容。

main 函数可能返回任何实现了 std::process::Termination 特质(the std::process::Termination 的任何类型,该特质包含了返回某个 ExitCodereport 函数。请参考标准库文档,了解更多有关实现自己类型 Termination 的信息。

现在既然已经讨论了调用 panic! 或返回 Result 的细节,那么就要回到怎样判断,在何种情形下,使用哪种方式属于恰当的话题了。

Last change: 2023-11-30, commit: 6aef347