Rust 用 Result 處理可恢復(fù)的錯(cuò)誤

2023-03-22 15:10 更新
ch09-02-recoverable-errors-with-result.md
commit 0bac27c66136764c82fe267763945f3c65eea002

大部分錯(cuò)誤并沒(méi)有嚴(yán)重到需要程序完全停止執(zhí)行。有時(shí),一個(gè)函數(shù)會(huì)因?yàn)橐粋€(gè)容易理解并做出反應(yīng)的原因失敗。例如,如果因?yàn)榇蜷_(kāi)一個(gè)并不存在的文件而失敗,此時(shí)我們可能想要?jiǎng)?chuàng)建這個(gè)文件,而不是終止進(jìn)程。

回憶一下第二章 “使用 ?Result ?類型來(lái)處理潛在的錯(cuò)誤” 部分中的那個(gè) Result 枚舉,它定義有如下兩個(gè)成員,Ok 和 Err

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

T 和 E 是泛型類型參數(shù);第十章會(huì)詳細(xì)介紹泛型?,F(xiàn)在你需要知道的就是 T 代表成功時(shí)返回的 Ok 成員中的數(shù)據(jù)的類型,而 E 代表失敗時(shí)返回的 Err 成員中的錯(cuò)誤的類型。因?yàn)?nbsp;Result 有這些泛型類型參數(shù),我們可以將 Result 類型和標(biāo)準(zhǔn)庫(kù)中為其定義的函數(shù)用于很多不同的場(chǎng)景,這些情況中需要返回的成功值和失敗值可能會(huì)各不相同。

讓我們調(diào)用一個(gè)返回 Result 的函數(shù),因?yàn)樗赡軙?huì)失?。喝缡纠?9-3 所示打開(kāi)一個(gè)文件:

文件名: src/main.rs

use std::fs::File;

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

示例 9-3:打開(kāi)文件

如何知道 File::open 返回一個(gè) Result 呢?我們可以查看 標(biāo)準(zhǔn)庫(kù) API 文檔,或者可以直接問(wèn)編譯器!如果給 f 某個(gè)我們知道 不是 函數(shù)返回值類型的類型注解,接著嘗試編譯代碼,編譯器會(huì)告訴我們類型不匹配。然后錯(cuò)誤信息會(huì)告訴我們 f 的類型 應(yīng)該 是什么。讓我們?cè)囋嚕∥覀冎?nbsp;File::open 的返回值不是 u32 類型的,所以將 let f 語(yǔ)句改為如下:

    let f: u32 = File::open("hello.txt");

現(xiàn)在嘗試編譯會(huì)給出如下輸出:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
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` due to previous error

這就告訴我們了 File::open 函數(shù)的返回值類型是 Result<T, E>。這里泛型參數(shù) T 放入了成功值的類型 std::fs::File,它是一個(gè)文件句柄。E 被用在失敗值上時(shí) E 的類型是 std::io::Error

這個(gè)返回值類型說(shuō)明 File::open 調(diào)用可能會(huì)成功并返回一個(gè)可以進(jìn)行讀寫的文件句柄。這個(gè)函數(shù)也可能會(huì)失敗:例如,文件可能并不存在,或者可能沒(méi)有訪問(wèn)文件的權(quán)限。File::open 需要一個(gè)方式告訴我們是成功還是失敗,并同時(shí)提供給我們文件句柄或錯(cuò)誤信息。而這些信息正是 Result 枚舉可以提供的。

當(dāng) File::open 成功的情況下,變量 f 的值將會(huì)是一個(gè)包含文件句柄的 Ok 實(shí)例。在失敗的情況下,f 的值會(huì)是一個(gè)包含更多關(guān)于出現(xiàn)了何種錯(cuò)誤信息的 Err 實(shí)例。

我們需要在示例 9-3 的代碼中增加根據(jù) File::open 返回值進(jìn)行不同處理的邏輯。示例 9-4 展示了一個(gè)使用基本工具處理 Result 的例子:第六章學(xué)習(xí)過(guò)的 match 表達(dá)式。

文件名: src/main.rs

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

示例 9-4:使用 match 表達(dá)式處理可能會(huì)返回的 Result 成員

注意與 Option 枚舉一樣,Result 枚舉和其成員也被導(dǎo)入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。

這里我們告訴 Rust 當(dāng)結(jié)果是 Ok 時(shí),返回 Ok 成員中的 file 值,然后將這個(gè)文件句柄賦值給變量 f。match 之后,我們可以利用這個(gè)文件句柄來(lái)進(jìn)行讀寫。

match 的另一個(gè)分支處理從 File::open 得到 Err 值的情況。在這種情況下,我們選擇調(diào)用 panic! 宏。如果當(dāng)前目錄沒(méi)有一個(gè)叫做 hello.txt 的文件,當(dāng)運(yùn)行這段代碼時(shí)會(huì)看到如下來(lái)自 panic! 宏的輸出:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

一如既往,此輸出準(zhǔn)確地告訴了我們到底出了什么錯(cuò)。

匹配不同的錯(cuò)誤

示例 9-4 中的代碼不管 File::open 是因?yàn)槭裁丛蚴《紩?huì) panic!。我們真正希望的是對(duì)不同的錯(cuò)誤原因采取不同的行為:如果 File::open 因?yàn)槲募淮嬖诙?,我們希望?chuàng)建這個(gè)文件并返回新文件的句柄。如果 File::open 因?yàn)槿魏纹渌蚴?,例如沒(méi)有打開(kāi)文件的權(quán)限,我們?nèi)匀幌M袷纠?9-4 那樣 panic!。讓我們看看示例 9-5,其中 match 增加了另一個(gè)分支:

文件名: 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(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}

示例 9-5:使用不同的方式處理不同類型的錯(cuò)誤

File::open 返回的 Err 成員中的值類型 io::Error,它是一個(gè)標(biāo)準(zhǔn)庫(kù)中提供的結(jié)構(gòu)體。這個(gè)結(jié)構(gòu)體有一個(gè)返回 io::ErrorKind 值的 kind 方法可供調(diào)用。io::ErrorKind 是一個(gè)標(biāo)準(zhǔn)庫(kù)提供的枚舉,它的成員對(duì)應(yīng) io 操作可能導(dǎo)致的不同錯(cuò)誤類型。我們感興趣的成員是 ErrorKind::NotFound,它代表嘗試打開(kāi)的文件并不存在。這樣,match 就匹配完 f 了,不過(guò)對(duì)于 error.kind() 還有一個(gè)內(nèi)層 match。

我們希望在內(nèi)層 match 中檢查的條件是 error.kind() 的返回值是否為 ErrorKind的 NotFound 成員。如果是,則嘗試通過(guò) File::create 創(chuàng)建文件。然而因?yàn)?nbsp;File::create 也可能會(huì)失敗,還需要增加一個(gè)內(nèi)層 match 語(yǔ)句。當(dāng)文件不能被打開(kāi),會(huì)打印出一個(gè)不同的錯(cuò)誤信息。外層 match 的最后一個(gè)分支保持不變,這樣對(duì)任何除了文件不存在的錯(cuò)誤會(huì)使程序 panic。

不同于使用 ?match ?和 ?Result<T, E>?

這里有好多 match!match 確實(shí)很強(qiáng)大,不過(guò)也非常的基礎(chǔ)。第十三章我們會(huì)介紹閉包(closure),這可以用于很多 Result<T, E> 上定義的方法。在處理代碼中的 Result<T, E> 值時(shí)這些方法可能會(huì)更加簡(jiǎn)潔。

例如,這是另一個(gè)編寫與示例 9-5 邏輯相同但是使用閉包和 unwrap_or_else 方法的例子:

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 一樣的行為,但并沒(méi)有包含任何 match 表達(dá)式且更容易閱讀。在閱讀完第十三章后再回到這個(gè)例子,并查看標(biāo)準(zhǔn)庫(kù)文檔 unwrap_or_else 方法都做了什么操作。在處理錯(cuò)誤時(shí),還有很多這類方法可以消除大量嵌套的 match 表達(dá)式。

失敗時(shí) panic 的簡(jiǎn)寫:unwrap 和 expect

match 能夠勝任它的工作,不過(guò)它可能有點(diǎn)冗長(zhǎng)并且不總是能很好的表明其意圖。Result<T, E> 類型定義了很多輔助方法來(lái)處理各種情況。其中之一叫做 unwrap,它的實(shí)現(xiàn)就類似于示例 9-4 中的 match 語(yǔ)句。如果 Result 值是成員 Ok,unwrap 會(huì)返回 Ok 中的值。如果 Result 是成員 Err,unwrap 會(huì)為我們調(diào)用 panic!。這里是一個(gè)實(shí)踐 unwrap 的例子:

文件名: src/main.rs

use std::fs::File;

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

如果調(diào)用這段代碼時(shí)不存在 hello.txt 文件,我們將會(huì)看到一個(gè) unwrap 調(diào)用 panic! 時(shí)提供的錯(cuò)誤信息:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

還有另一個(gè)類似于 unwrap 的方法它還允許我們選擇 panic! 的錯(cuò)誤信息:expect。使用 expect 而不是 unwrap 并提供一個(gè)好的錯(cuò)誤信息可以表明你的意圖并更易于追蹤 panic 的根源。expect 的語(yǔ)法看起來(lái)像這樣:

文件名: src/main.rs

use std::fs::File;

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

expect 與 unwrap 的使用方式一樣:返回文件句柄或調(diào)用 panic! 宏。expect 在調(diào)用 panic! 時(shí)使用的錯(cuò)誤信息將是我們傳遞給 expect 的參數(shù),而不像 unwrap 那樣使用默認(rèn)的 panic! 信息。它看起來(lái)像這樣:

thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

因?yàn)檫@個(gè)錯(cuò)誤信息以我們指定的文本開(kāi)始,Failed to open hello.txt,將會(huì)更容易找到代碼中的錯(cuò)誤信息來(lái)自何處。如果在多處使用 unwrap,則需要花更多的時(shí)間來(lái)分析到底是哪一個(gè) unwrap 造成了 panic,因?yàn)樗械?nbsp;unwrap 調(diào)用都打印相同的信息。

傳播錯(cuò)誤

當(dāng)編寫一個(gè)其實(shí)先會(huì)調(diào)用一些可能會(huì)失敗的操作的函數(shù)時(shí),除了在這個(gè)函數(shù)中處理錯(cuò)誤外,還可以選擇讓調(diào)用者知道這個(gè)錯(cuò)誤并決定該如何處理。這被稱為 傳播propagating)錯(cuò)誤,這樣能更好的控制代碼調(diào)用,因?yàn)楸绕鹉愦a所擁有的上下文,調(diào)用者可能擁有更多信息或邏輯來(lái)決定應(yīng)該如何處理錯(cuò)誤。

例如,示例 9-6 展示了一個(gè)從文件中讀取用戶名的函數(shù)。如果文件不存在或不能讀取,這個(gè)函數(shù)會(huì)將這些錯(cuò)誤返回給調(diào)用它的代碼:

文件名: src/main.rs

use std::fs::File;
use std::io::{self, Read};

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) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

示例 9-6:一個(gè)函數(shù)使用 match 將錯(cuò)誤返回給代碼調(diào)用者

首先讓我們看看函數(shù)的返回值:Result<String, io::Error>。這意味著函數(shù)返回一個(gè) Result<T, E> 類型的值,其中泛型參數(shù) T 的具體類型是 String,而 E 的具體類型是 io::Error。如果這個(gè)函數(shù)沒(méi)有出任何錯(cuò)誤成功返回,函數(shù)的調(diào)用者會(huì)收到一個(gè)包含 String 的 Ok 值 —— 函數(shù)從文件中讀取到的用戶名。如果函數(shù)遇到任何錯(cuò)誤,函數(shù)的調(diào)用者會(huì)收到一個(gè) Err 值,它儲(chǔ)存了一個(gè)包含更多這個(gè)問(wèn)題相關(guān)信息的 io::Error 實(shí)例。這里選擇 io::Error 作為函數(shù)的返回值是因?yàn)樗檬呛瘮?shù)體中那兩個(gè)可能會(huì)失敗的操作的錯(cuò)誤返回值:File::open 函數(shù)和 read_to_string 方法。

函數(shù)體以調(diào)用 File::open 函數(shù)開(kāi)始。接著使用 match 處理返回值 Result,類似示例 9-4,如果 File::open 成功了,模式變量 file 中的文件句柄就變成了可變變量 f 中的值,接著函數(shù)繼續(xù)執(zhí)行。在 Err 的情況下,我們沒(méi)有調(diào)用 panic!,而是使用 return 關(guān)鍵字提前結(jié)束整個(gè)函數(shù),并將來(lái)自 File::open 的錯(cuò)誤值(現(xiàn)在在模式變量 e 中)作為函數(shù)的錯(cuò)誤值傳回給調(diào)用者。

所以 f 中有了一個(gè)文件句柄,函數(shù)接著在變量 s 中創(chuàng)建了一個(gè)新 String 并調(diào)用文件句柄 f 的 read_to_string 方法來(lái)將文件的內(nèi)容讀取到 s 中。read_to_string 方法也返回一個(gè) Result 因?yàn)樗部赡軙?huì)失?。耗呐率?nbsp;File::open 已經(jīng)成功了。所以我們需要另一個(gè) match 來(lái)處理這個(gè) Result:如果 read_to_string 成功了,那么這個(gè)函數(shù)就成功了,并返回文件中的用戶名,它現(xiàn)在位于被封裝進(jìn) Ok 的 s 中。如果read_to_string 失敗了,則像之前處理 File::open 的返回值的 match 那樣返回錯(cuò)誤值。不過(guò)并不需要顯式的調(diào)用 return,因?yàn)檫@是函數(shù)的最后一個(gè)表達(dá)式。

調(diào)用這個(gè)函數(shù)的代碼最終會(huì)得到一個(gè)包含用戶名的 Ok 值,或者一個(gè)包含 io::Error 的 Err 值。我們無(wú)從得知調(diào)用者會(huì)如何處理這些值。例如,如果他們得到了一個(gè) Err 值,他們可能會(huì)選擇 panic! 并使程序崩潰、使用一個(gè)默認(rèn)的用戶名或者從文件之外的地方尋找用戶名。我們沒(méi)有足夠的信息知曉調(diào)用者具體會(huì)如何嘗試,所以將所有的成功或失敗信息向上傳播,讓他們選擇合適的處理方法。

這種傳播錯(cuò)誤的模式在 Rust 是如此的常見(jiàn),以至于 Rust 提供了 ? 問(wèn)號(hào)運(yùn)算符來(lái)使其更易于處理。

傳播錯(cuò)誤的簡(jiǎn)寫:? 運(yùn)算符

示例 9-7 展示了一個(gè) read_username_from_file 的實(shí)現(xiàn),它實(shí)現(xiàn)了與示例 9-6 中的代碼相同的功能,不過(guò)這個(gè)實(shí)現(xiàn)使用了 ? 運(yùn)算符:

文件名: src/main.rs

use std::fs::File;
use std::io;
use std::io::Read;

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

示例 9-7:一個(gè)使用 ? 運(yùn)算符向調(diào)用者返回錯(cuò)誤的函數(shù)

Result 值之后的 ? 被定義為與示例 9-6 中定義的處理 Result 值的 match 表達(dá)式有著完全相同的工作方式。如果 Result 的值是 Ok,這個(gè)表達(dá)式將會(huì)返回 Ok 中的值而程序?qū)⒗^續(xù)執(zhí)行。如果值是 ErrErr 中的值將作為整個(gè)函數(shù)的返回值,就好像使用了 return 關(guān)鍵字一樣,這樣錯(cuò)誤值就被傳播給了調(diào)用者。

示例 9-6 中的 match 表達(dá)式與問(wèn)號(hào)運(yùn)算符所做的有一點(diǎn)不同:? 運(yùn)算符所使用的錯(cuò)誤值被傳遞給了 from 函數(shù),它定義于標(biāo)準(zhǔn)庫(kù)的 From trait 中,其用來(lái)將錯(cuò)誤從一種類型轉(zhuǎn)換為另一種類型。當(dāng) ? 運(yùn)算符調(diào)用 from 函數(shù)時(shí),收到的錯(cuò)誤類型被轉(zhuǎn)換為由當(dāng)前函數(shù)返回類型所指定的錯(cuò)誤類型。這在當(dāng)函數(shù)返回單個(gè)錯(cuò)誤類型來(lái)代表所有可能失敗的方式時(shí)很有用,即使其可能會(huì)因很多種原因失敗。只要每一個(gè)錯(cuò)誤類型都實(shí)現(xiàn)了 from 函數(shù)來(lái)定義如何將自身轉(zhuǎn)換為返回的錯(cuò)誤類型,? 運(yùn)算符會(huì)自動(dòng)處理這些轉(zhuǎn)換。

在示例 9-7 的上下文中,File::open 調(diào)用結(jié)尾的 ? 將會(huì)把 Ok 中的值返回給變量 f。如果出現(xiàn)了錯(cuò)誤,? 運(yùn)算符會(huì)提早返回整個(gè)函數(shù)并將一些 Err 值傳播給調(diào)用者。同理也適用于 read_to_string 調(diào)用結(jié)尾的 ?。

? 運(yùn)算符消除了大量樣板代碼并使得函數(shù)的實(shí)現(xiàn)更簡(jiǎn)單。我們甚至可以在 ? 之后直接使用鏈?zhǔn)椒椒ㄕ{(diào)用來(lái)進(jìn)一步縮短代碼,如示例 9-8 所示:

文件名: src/main.rs

use std::fs::File;
use std::io;
use std::io::Read;

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-8:?jiǎn)柼?hào)運(yùn)算符之后的鏈?zhǔn)椒椒ㄕ{(diào)用

在 s 中創(chuàng)建新的 String 被放到了函數(shù)開(kāi)頭;這一部分沒(méi)有變化。我們對(duì) File::open("hello.txt")? 的結(jié)果直接鏈?zhǔn)秸{(diào)用了 read_to_string,而不再創(chuàng)建變量 f。仍然需要 read_to_string 調(diào)用結(jié)尾的 ?,而且當(dāng) File::open 和 read_to_string 都成功沒(méi)有失敗時(shí)返回包含用戶名 s 的 Ok 值。其功能再一次與示例 9-6 和示例 9-7 保持一致,不過(guò)這是一個(gè)與眾不同且更符合工程學(xué)(ergonomic)的寫法。

說(shuō)到編寫這個(gè)函數(shù)的不同方法,甚至還有一個(gè)更短的寫法:

文件名: src/main.rs

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

將文件讀取到一個(gè)字符串是相當(dāng)常見(jiàn)的操作,所以 Rust 提供了名為 fs::read_to_string 的函數(shù),它會(huì)打開(kāi)文件、新建一個(gè) String、讀取文件的內(nèi)容,并將內(nèi)容放入 String,接著返回它。當(dāng)然,這樣做就沒(méi)有展示所有這些錯(cuò)誤處理的機(jī)會(huì)了,所以我們最初就選擇了艱苦的道路。

哪里可以使用 ? 運(yùn)算符

? 運(yùn)算符只能被用于返回值與 ? 作用的值相兼容的函數(shù)。因?yàn)?nbsp;? 運(yùn)算符被定義為從函數(shù)中提早返回一個(gè)值,這與示例 9-6 中的 match 表達(dá)式有著完全相同的工作方式。示例 9-6 中 match 作用于一個(gè) Result 值,提早返回的分支返回了一個(gè) Err(e) 值。函數(shù)的返回值必須是 Result 才能與這個(gè) return 相兼容。

在示例 9-10 中,讓我們看看在返回值不兼容的 main 函數(shù)中使用 ? 運(yùn)算符會(huì)得到什么錯(cuò)誤:

use std::fs::File;

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

示例 9-10: 嘗試在返回 () 的 main 函數(shù)中使用 ? 的代碼不能編譯

這段代碼打開(kāi)一個(gè)文件,這可能會(huì)失敗。? 運(yùn)算符作用于 File::open 返回的 Result 值,不過(guò) main 函數(shù)的返回類型是 () 而不是 Result。當(dāng)編譯這些代碼,會(huì)得到如下錯(cuò)誤信息:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
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:36
  |
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 `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` due to previous error

這個(gè)錯(cuò)誤指出只能在返回 Result 或者其它實(shí)現(xiàn)了 FromResidual 的類型的函數(shù)中使用 ? 運(yùn)算符。為了修復(fù)這個(gè)錯(cuò)誤,有兩個(gè)選擇。一個(gè)是,如果沒(méi)有限制的話將函數(shù)的返回值改為 Result<T, E>。另一個(gè)是使用 match 或 Result<T, E> 的方法中合適的一個(gè)來(lái)處理 Result<T, E>

錯(cuò)誤信息也提到 ? 也可用于 Option<T> 值。如同對(duì) Result 使用 ? 一樣,只能在返回 Option 的函數(shù)中對(duì) Option 使用 ?。在 Option<T> 上調(diào)用 ? 運(yùn)算符的行為與 Result<T, E> 類似:如果值是 None,此時(shí) None 會(huì)從函數(shù)中提前返回。如果值是 Some,Some 中的值作為表達(dá)式的返回值同時(shí)函數(shù)繼續(xù)。示例 9-11 中有一個(gè)從給定文本中返回第一行最后一個(gè)字符的函數(shù)的例子:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

示例 9-11: 在 Option<T> 值上使用 ? 運(yùn)算符

這個(gè)函數(shù)返回 Option<char> 因?yàn)樗赡軙?huì)在這個(gè)位置找到一個(gè)字符,也可能沒(méi)有字符。這段代碼獲取 text 字符串 slice 作為參數(shù)并調(diào)用其 lines 方法,這會(huì)返回一個(gè)字符串中每一行的迭代器。因?yàn)楹瘮?shù)希望檢查第一行,所以調(diào)用了迭代器 next 來(lái)獲取迭代器中第一個(gè)值。如果 text 是空字符串,next 調(diào)用會(huì)返回 None,此時(shí)我們可以使用 ? 來(lái)停止并從 last_char_of_first_line 返回 None。如果 text 不是空字符串,next 會(huì)返回一個(gè)包含 text 中第一行的字符串 slice 的 Some 值。

? 會(huì)提取這個(gè)字符串 slice,然后可以在字符串 slice 上調(diào)用 chars 來(lái)獲取字符的迭代器。我們感興趣的是第一行的最后一個(gè)字符,所以可以調(diào)用 last 來(lái)返回迭代器的最后一項(xiàng)。這是一個(gè) Option,因?yàn)橛锌赡艿谝恍惺且粋€(gè)空字符串,例如 text 以一個(gè)空行開(kāi)頭而后面的行有文本,像是 "\nhi"。不過(guò),如果第一行有最后一個(gè)字符,它會(huì)返回在一個(gè) Some 成員中。? 運(yùn)算符作用于其中給了我們一個(gè)簡(jiǎn)潔的表達(dá)這種邏輯的方式。如果我們不能在 Option 上使用 ? 運(yùn)算符,則不得不使用更多的方法調(diào)用或者 match 表達(dá)式來(lái)實(shí)現(xiàn)這些邏輯。

注意你可以在返回 Result 的函數(shù)中對(duì) Result 使用 ? 運(yùn)算符,可以在返回 Option 的函數(shù)中對(duì) Option 使用 ? 運(yùn)算符,但是不可以混合搭配。? 運(yùn)算符不會(huì)自動(dòng)將 Result 轉(zhuǎn)化為 Option,反之亦然;在這些情況下,可以使用類似 Result 的 ok 方法或者 Option 的 ok_or 方法來(lái)顯式轉(zhuǎn)換。

目前為止,我們所使用的所有 main 函數(shù)都返回 ()。main 函數(shù)是特殊的因?yàn)樗强蓤?zhí)行程序的入口點(diǎn)和退出點(diǎn),為了使程序能正常工作,其可以返回的類型是有限制的。

幸運(yùn)的是 main 函數(shù)也可以返回 Result<(), E>, 示例 9-12 中的代碼來(lái)自示例 9-10 不過(guò)修改了 main 的返回值為 Result<(), Box<dyn Error>> 并在結(jié)尾增加了一個(gè) Ok(()) 作為返回值。這段代碼可以編譯:

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

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

    Ok(())
}

示例 9-12: 修改 main 返回 Result<(), E> 允許對(duì) Result 值使用 ? 運(yùn)算符

Box<dyn Error> 類型是一個(gè) trait 對(duì)象trait object)第十七章 “為使用不同類型的值而設(shè)計(jì)的 trait 對(duì)象” 部分會(huì)做介紹。目前可以將 Box<dyn Error> 理解為 “任何類型的錯(cuò)誤”。在返回 Box<dyn Error> 錯(cuò)誤類型 main 函數(shù)中對(duì) Result 使用 ? 是允許的,因?yàn)樗试S任何 Err 值提前返回。

當(dāng) main 函數(shù)返回 Result<(), E>,如果 main 返回 Ok(()) 可執(zhí)行程序會(huì)以 0 值退出,而如果 main 返回 Err 值則會(huì)以非零值退出;成功退出的程序會(huì)返回整數(shù) 0,運(yùn)行錯(cuò)誤的程序會(huì)返回非 0 的整數(shù)。Rust 也會(huì)從二進(jìn)制程序中返回與這個(gè)慣例相兼容的整數(shù)。

main 函數(shù)也可以返回任何實(shí)現(xiàn)了 std::process::Termination trait 的類型。截至本書編寫時(shí),Termination trait 是一個(gè)不穩(wěn)定功能(unstable feature),只能用于 Nightly Rust 中,所以你不能在 穩(wěn)定版 Rust(Stable Rust)中用自己的類型去實(shí)現(xiàn),不過(guò)有朝一日應(yīng)該可以!

現(xiàn)在我們討論過(guò)了調(diào)用 panic! 或返回 Result 的細(xì)節(jié),是時(shí)候回到他們各自適合哪些場(chǎng)景的話題了。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)