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

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

大部分錯誤并沒有嚴重到需要程序完全停止執(zhí)行。有時,一個函數(shù)會因為一個容易理解并做出反應(yīng)的原因失敗。例如,如果因為打開一個并不存在的文件而失敗,此時我們可能想要創(chuàng)建這個文件,而不是終止進程。

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

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

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

讓我們調(diào)用一個返回 Result 的函數(shù),因為它可能會失?。喝缡纠?9-3 所示打開一個文件:

文件名: src/main.rs

use std::fs::File;

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

示例 9-3:打開文件

如何知道 File::open 返回一個 Result 呢?我們可以查看 標準庫 API 文檔,或者可以直接問編譯器!如果給 f 某個我們知道 不是 函數(shù)返回值類型的類型注解,接著嘗試編譯代碼,編譯器會告訴我們類型不匹配。然后錯誤信息會告訴我們 f 的類型 應(yīng)該 是什么。讓我們試試!我們知道 File::open 的返回值不是 u32 類型的,所以將 let f 語句改為如下:

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

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

$ 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,它是一個文件句柄。E 被用在失敗值上時 E 的類型是 std::io::Error。

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

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

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

文件名: 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 表達式處理可能會返回的 Result 成員

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

這里我們告訴 Rust 當結(jié)果是 Ok 時,返回 Ok 成員中的 file 值,然后將這個文件句柄賦值給變量 fmatch 之后,我們可以利用這個文件句柄來進行讀寫。

match 的另一個分支處理從 File::open 得到 Err 值的情況。在這種情況下,我們選擇調(diào)用 panic! 宏。如果當前目錄沒有一個叫做 hello.txt 的文件,當運行這段代碼時會看到如下來自 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

一如既往,此輸出準確地告訴了我們到底出了什么錯。

匹配不同的錯誤

示例 9-4 中的代碼不管 File::open 是因為什么原因失敗都會 panic!。我們真正希望的是對不同的錯誤原因采取不同的行為:如果 File::open 因為文件不存在而失敗,我們希望創(chuàng)建這個文件并返回新文件的句柄。如果 File::open 因為任何其他原因失敗,例如沒有打開文件的權(quán)限,我們?nèi)匀幌M袷纠?9-4 那樣 panic!。讓我們看看示例 9-5,其中 match 增加了另一個分支:

文件名: 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:使用不同的方式處理不同類型的錯誤

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

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

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

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

例如,這是另一個編寫與示例 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 一樣的行為,但并沒有包含任何 match 表達式且更容易閱讀。在閱讀完第十三章后再回到這個例子,并查看標準庫文檔 unwrap_or_else 方法都做了什么操作。在處理錯誤時,還有很多這類方法可以消除大量嵌套的 match 表達式。

失敗時 panic 的簡寫:unwrap 和 expect

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

文件名: src/main.rs

use std::fs::File;

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

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

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

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

文件名: 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! 時使用的錯誤信息將是我們傳遞給 expect 的參數(shù),而不像 unwrap 那樣使用默認的 panic! 信息。它看起來像這樣:

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

因為這個錯誤信息以我們指定的文本開始,Failed to open hello.txt,將會更容易找到代碼中的錯誤信息來自何處。如果在多處使用 unwrap,則需要花更多的時間來分析到底是哪一個 unwrap 造成了 panic,因為所有的 unwrap 調(diào)用都打印相同的信息。

傳播錯誤

當編寫一個其實先會調(diào)用一些可能會失敗的操作的函數(shù)時,除了在這個函數(shù)中處理錯誤外,還可以選擇讓調(diào)用者知道這個錯誤并決定該如何處理。這被稱為 傳播propagating)錯誤,這樣能更好的控制代碼調(diào)用,因為比起你代碼所擁有的上下文,調(diào)用者可能擁有更多信息或邏輯來決定應(yīng)該如何處理錯誤。

例如,示例 9-6 展示了一個從文件中讀取用戶名的函數(shù)。如果文件不存在或不能讀取,這個函數(shù)會將這些錯誤返回給調(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:一個函數(shù)使用 match 將錯誤返回給代碼調(diào)用者

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

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

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

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

這種傳播錯誤的模式在 Rust 是如此的常見,以至于 Rust 提供了 ? 問號運算符來使其更易于處理。

傳播錯誤的簡寫:? 運算符

示例 9-7 展示了一個 read_username_from_file 的實現(xiàn),它實現(xiàn)了與示例 9-6 中的代碼相同的功能,不過這個實現(xià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:一個使用 ? 運算符向調(diào)用者返回錯誤的函數(shù)

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

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

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

? 運算符消除了大量樣板代碼并使得函數(shù)的實現(xiàn)更簡單。我們甚至可以在 ? 之后直接使用鏈式方法調(diào)用來進一步縮短代碼,如示例 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:問號運算符之后的鏈式方法調(diào)用

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

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

文件名: 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

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

哪里可以使用 ? 運算符

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

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

use std::fs::File;

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

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

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

$ 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

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

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

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

示例 9-11: 在 Option<T> 值上使用 ? 運算符

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

? 會提取這個字符串 slice,然后可以在字符串 slice 上調(diào)用 chars 來獲取字符的迭代器。我們感興趣的是第一行的最后一個字符,所以可以調(diào)用 last 來返回迭代器的最后一項。這是一個 Option,因為有可能第一行是一個空字符串,例如 text 以一個空行開頭而后面的行有文本,像是 "\nhi"。不過,如果第一行有最后一個字符,它會返回在一個 Some 成員中。? 運算符作用于其中給了我們一個簡潔的表達這種邏輯的方式。如果我們不能在 Option 上使用 ? 運算符,則不得不使用更多的方法調(diào)用或者 match 表達式來實現(xiàn)這些邏輯。

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

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

幸運的是 main 函數(shù)也可以返回 Result<(), E>, 示例 9-12 中的代碼來自示例 9-10 不過修改了 main 的返回值為 Result<(), Box<dyn Error>> 并在結(jié)尾增加了一個 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> 允許對 Result 值使用 ? 運算符

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號