Rust 重構(gòu)改進(jìn)模塊性和錯(cuò)誤處理

2023-03-22 15:11 更新
ch12-03-improving-error-handling-and-modularity.md
commit c8a9ac9cee7923422b2eceebf0375363440dbfc1

為了改善我們的程序這里有四個(gè)問題需要修復(fù),而且他們都與程序的組織方式和如何處理潛在錯(cuò)誤有關(guān)。

第一,main 現(xiàn)在進(jìn)行了兩個(gè)任務(wù):它解析了參數(shù)并打開了文件。對(duì)于一個(gè)這樣的小函數(shù),這并不是一個(gè)大問題。然而如果 main 中的功能持續(xù)增加,main 函數(shù)處理的獨(dú)立任務(wù)也會(huì)增加。當(dāng)函數(shù)承擔(dān)了更多責(zé)任,它就更難以推導(dǎo),更難以測(cè)試,并且更難以在不破壞其他部分的情況下做出修改。最好能分離出功能以便每個(gè)函數(shù)就負(fù)責(zé)一個(gè)任務(wù)。

這同時(shí)也關(guān)系到第二個(gè)問題:query 和 filename 是程序中的配置變量,而像 contents 則用來執(zhí)行程序邏輯。隨著 main 函數(shù)的增長(zhǎng),就需要引入更多的變量到作用域中,而當(dāng)作用域中有更多的變量時(shí),將更難以追蹤每個(gè)變量的目的。最好能將配置變量組織進(jìn)一個(gè)結(jié)構(gòu),這樣就能使他們的目的更明確了。

第三個(gè)問題是如果打開文件失敗我們使用 expect 來打印出錯(cuò)誤信息,不過這個(gè)錯(cuò)誤信息只是說 Something went wrong reading the file。讀取文件失敗的原因有多種:例如文件不存在,或者沒有打開此文件的權(quán)限。目前,無論處于何種情況,我們只是打印出“文件讀取出現(xiàn)錯(cuò)誤”的信息,這并沒有給予使用者具體的信息!

第四,我們不停地使用 expect 來處理不同的錯(cuò)誤,如果用戶沒有指定足夠的參數(shù)來運(yùn)行程序,他們會(huì)從 Rust 得到 index out of bounds 錯(cuò)誤,而這并不能明確地解釋問題。如果所有的錯(cuò)誤處理都位于一處,這樣將來的維護(hù)者在需要修改錯(cuò)誤處理邏輯時(shí)就只需要考慮這一處代碼。將所有的錯(cuò)誤處理都放在一處也有助于確保我們打印的錯(cuò)誤信息對(duì)終端用戶來說是有意義的。

讓我們通過重構(gòu)項(xiàng)目來解決這些問題。

二進(jìn)制項(xiàng)目的關(guān)注分離

main 函數(shù)負(fù)責(zé)多個(gè)任務(wù)的組織問題在許多二進(jìn)制項(xiàng)目中很常見。所以 Rust 社區(qū)開發(fā)出一類在 main 函數(shù)開始變得龐大時(shí)進(jìn)行二進(jìn)制程序的關(guān)注分離的指導(dǎo)性過程。這些過程有如下步驟:

  • 將程序拆分成 main.rs 和 lib.rs 并將程序的邏輯放入 lib.rs 中。
  • 當(dāng)命令行解析邏輯比較小時(shí),可以保留在 main.rs 中。
  • 當(dāng)命令行解析開始變得復(fù)雜時(shí),也同樣將其從 main.rs 提取到 lib.rs 中。

經(jīng)過這些過程之后保留在 main 函數(shù)中的責(zé)任應(yīng)該被限制為:

  • 使用參數(shù)值調(diào)用命令行解析邏輯
  • 設(shè)置任何其他的配置
  • 調(diào)用 lib.rs 中的 ?run? 函數(shù)
  • 如果 ?run? 返回錯(cuò)誤,則處理這個(gè)錯(cuò)誤

這個(gè)模式的一切就是為了關(guān)注分離:main.rs 處理程序運(yùn)行,而 lib.rs 處理所有的真正的任務(wù)邏輯。因?yàn)椴荒苤苯訙y(cè)試 main 函數(shù),這個(gè)結(jié)構(gòu)通過將所有的程序邏輯移動(dòng)到 lib.rs 的函數(shù)中使得我們可以測(cè)試他們。僅僅保留在 main.rs 中的代碼將足夠小以便閱讀就可以驗(yàn)證其正確性。讓我們遵循這些步驟來重構(gòu)程序。

提取參數(shù)解析器

首先,我們將解析參數(shù)的功能提取到一個(gè) main 將會(huì)調(diào)用的函數(shù)中,為將命令行解析邏輯移動(dòng)到 src/lib.rs 中做準(zhǔn)備。示例 12-5 中展示了新 main 函數(shù)的開頭,它調(diào)用了新函數(shù) parse_config。目前它仍將定義在 src/main.rs 中:

文件名: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, filename) = parse_config(&args);

    // --snip--
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let filename = &args[2];

    (query, filename)
}

示例 12-5:從 main 中提取出 parse_config 函數(shù)

我們?nèi)匀粚⒚钚袇?shù)收集進(jìn)一個(gè) vector,不過不同于在 main 函數(shù)中將索引 1 的參數(shù)值賦值給變量 query 和將索引 2 的值賦值給變量 filename,我們將整個(gè) vector 傳遞給 parse_config 函數(shù)。接著 parse_config 函數(shù)將包含決定哪個(gè)參數(shù)該放入哪個(gè)變量的邏輯,并將這些值返回到 main。仍然在 main 中創(chuàng)建變量 query 和 filename,不過 main 不再負(fù)責(zé)處理命令行參數(shù)與變量如何對(duì)應(yīng)。

這對(duì)重構(gòu)我們這小程序可能有點(diǎn)大材小用,不過我們將采用小的、增量的步驟進(jìn)行重構(gòu)。在做出這些改變之后,再次運(yùn)行程序并驗(yàn)證參數(shù)解析是否仍然正常。經(jīng)常驗(yàn)證你的進(jìn)展是一個(gè)好習(xí)慣,這樣在遇到問題時(shí)能幫助你定位問題的成因。

組合配置值

我們可以采取另一個(gè)小的步驟來進(jìn)一步改善這個(gè)函數(shù)?,F(xiàn)在函數(shù)返回一個(gè)元組,不過立刻又將元組拆成了獨(dú)立的部分。這是一個(gè)我們可能沒有進(jìn)行正確抽象的信號(hào)。

另一個(gè)表明還有改進(jìn)空間的跡象是 parse_config 名稱的 config 部分,它暗示了我們返回的兩個(gè)值是相關(guān)的并都是一個(gè)配置值的一部分。目前除了將這兩個(gè)值組合進(jìn)元組之外并沒有表達(dá)這個(gè)數(shù)據(jù)結(jié)構(gòu)的意義:我們可以將這兩個(gè)值放入一個(gè)結(jié)構(gòu)體并給每個(gè)字段一個(gè)有意義的名字。這會(huì)讓未來的維護(hù)者更容易理解不同的值如何相互關(guān)聯(lián)以及他們的目的。

注意:一些同學(xué)將這種在復(fù)雜類型更為合適的場(chǎng)景下使用基本類型的反模式稱為 基本類型偏執(zhí)primitive obsession)。

示例 12-6 展示了 parse_config 函數(shù)的改進(jìn)。

文件名: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    // --snip--
}

struct Config {
    query: String,
    filename: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

示例 12-6:重構(gòu) parse_config 返回一個(gè) Config 結(jié)構(gòu)體實(shí)例

新定義的結(jié)構(gòu)體 Config 中包含字段 query 和 filename。 parse_config 的簽名表明它現(xiàn)在返回一個(gè) Config 值。在之前的 parse_config 函數(shù)體中,我們返回了引用 args 中 String 值的字符串 slice,現(xiàn)在我們定義 Config 來包含擁有所有權(quán)的 String 值。main 中的 args 變量是參數(shù)值的所有者并只允許 parse_config 函數(shù)借用他們,這意味著如果 Config 嘗試獲取 args 中值的所有權(quán)將違反 Rust 的借用規(guī)則。

還有許多不同的方式可以處理 String 的數(shù)據(jù),而最簡(jiǎn)單但有些不太高效的方式是調(diào)用這些值的 clone 方法。這會(huì)生成 Config 實(shí)例可以擁有的數(shù)據(jù)的完整拷貝,不過會(huì)比儲(chǔ)存字符串?dāng)?shù)據(jù)的引用消耗更多的時(shí)間和內(nèi)存。不過拷貝數(shù)據(jù)使得代碼顯得更加直白因?yàn)闊o需管理引用的生命周期,所以在這種情況下犧牲一小部分性能來換取簡(jiǎn)潔性的取舍是值得的。

使用 clone 的權(quán)衡取舍

由于其運(yùn)行時(shí)消耗,許多 Rustacean 之間有一個(gè)趨勢(shì)是傾向于避免使用 clone 來解決所有權(quán)問題。在關(guān)于迭代器的第十三章中,我們將會(huì)學(xué)習(xí)如何更有效率的處理這種情況,不過現(xiàn)在,復(fù)制一些字符串來取得進(jìn)展是沒有問題的,因?yàn)橹粫?huì)進(jìn)行一次這樣的拷貝,而且文件名和要搜索的字符串都比較短。在第一輪編寫時(shí)擁有一個(gè)可以工作但有點(diǎn)低效的程序要比嘗試過度優(yōu)化代碼更好一些。隨著你對(duì) Rust 更加熟練,將能更輕松的直奔合適的方法,不過現(xiàn)在調(diào)用 clone 是完全可以接受的。

我們更新 main 將 parse_config 返回的 Config 實(shí)例放入變量 config 中,并將之前分別使用 query 和 filename 變量的代碼更新為現(xiàn)在的使用 Config 結(jié)構(gòu)體的字段的代碼。

現(xiàn)在代碼更明確的表現(xiàn)了我們的意圖,query 和 filename 是相關(guān)聯(lián)的并且他們的目的是配置程序如何工作。任何使用這些值的代碼就知道在 config 實(shí)例中對(duì)應(yīng)目的的字段名中尋找他們。

創(chuàng)建一個(gè) Config 的構(gòu)造函數(shù)

目前為止,我們將負(fù)責(zé)解析命令行參數(shù)的邏輯從 main 提取到了 parse_config 函數(shù)中,這有助于我們看清值 query 和 filename 是相互關(guān)聯(lián)的并應(yīng)該在代碼中表現(xiàn)這種關(guān)系。接著我們?cè)黾恿?nbsp;Config 結(jié)構(gòu)體來描述 query 和 filename 的相關(guān)性,并能夠從 parse_config 函數(shù)中將這些值的名稱作為結(jié)構(gòu)體字段名稱返回。

所以現(xiàn)在 parse_config 函數(shù)的目的是創(chuàng)建一個(gè) Config 實(shí)例,我們可以將 parse_config 從一個(gè)普通函數(shù)變?yōu)橐粋€(gè)叫做 new 的與結(jié)構(gòu)體關(guān)聯(lián)的函數(shù)。做出這個(gè)改變使得代碼更符合習(xí)慣:可以像標(biāo)準(zhǔn)庫(kù)中的 String 調(diào)用 String::new 來創(chuàng)建一個(gè)該類型的實(shí)例那樣,將 parse_config 變?yōu)橐粋€(gè)與 Config 關(guān)聯(lián)的 new 函數(shù)。示例 12-7 展示了需要做出的修改:

文件名: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    // --snip--
}

// --snip--

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

示例 12-7:將 parse_config 變?yōu)?nbsp;Config::new

這里將 main 中調(diào)用 parse_config 的地方更新為調(diào)用 Config::new。我們將 parse_config 的名字改為 new 并將其移動(dòng)到 impl 塊中,這使得 new 函數(shù)與 Config 相關(guān)聯(lián)。再次嘗試編譯并確保它可以工作。

修復(fù)錯(cuò)誤處理

現(xiàn)在我們開始修復(fù)錯(cuò)誤處理?;貞浺幌轮疤岬竭^如果 args vector 包含少于 3 個(gè)項(xiàng)并嘗試訪問 vector 中索引 1 或索引 2 的值會(huì)造成程序 panic。嘗試不帶任何參數(shù)運(yùn)行程序;這將看起來像這樣:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

index out of bounds: the len is 1 but the index is 1 是一個(gè)針對(duì)程序員的錯(cuò)誤信息,然而這并不能真正幫助終端用戶理解發(fā)生了什么和他們應(yīng)該做什么?,F(xiàn)在就讓我們修復(fù)它吧。

改善錯(cuò)誤信息

在示例 12-8 中,在 new 函數(shù)中增加了一個(gè)檢查在訪問索引 1 和 2 之前檢查 slice 是否足夠長(zhǎng)。如果 slice 不夠長(zhǎng),我們使用一個(gè)更好的錯(cuò)誤信息 panic 而不是 index out of bounds 信息:

文件名: src/main.rs

    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

示例 12-8:增加一個(gè)參數(shù)數(shù)量檢查

這類似于 示例 9-13 中的 Guess::new 函數(shù),那里如果 value 參數(shù)超出了有效值的范圍就調(diào)用 panic!。不同于檢查值的范圍,這里檢查 args 的長(zhǎng)度至少是 3,而函數(shù)的剩余部分則可以在假設(shè)這個(gè)條件成立的基礎(chǔ)上運(yùn)行。如果 args 少于 3 個(gè)項(xiàng),則這個(gè)條件將為真,并調(diào)用 panic! 立即終止程序。

有了 new 中這幾行額外的代碼,再次不帶任何參數(shù)運(yùn)行程序并看看現(xiàn)在錯(cuò)誤看起來像什么:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

這個(gè)輸出就好多了,現(xiàn)在有了一個(gè)合理的錯(cuò)誤信息。然而,還是有一堆額外的信息我們不希望提供給用戶。所以在這里使用示例 9-9 中的技術(shù)可能不是最好的;正如 第九章 所講到的一樣,panic! 的調(diào)用更趨向于程序上的問題而不是使用上的問題。相反我們可以使用第九章學(xué)習(xí)的另一個(gè)技術(shù) —— 返回一個(gè)可以表明成功或錯(cuò)誤的 Result。

從 new 中返回 Result 而不是調(diào)用 panic!

我們可以選擇返回一個(gè) Result 值,它在成功時(shí)會(huì)包含一個(gè) Config 的實(shí)例,而在錯(cuò)誤時(shí)會(huì)描述問題。當(dāng) Config::new 與 main 交流時(shí),可以使用 Result 類型來表明這里存在問題。接著修改 main 將 Err 成員轉(zhuǎn)換為對(duì)用戶更友好的錯(cuò)誤,而不是 panic! 調(diào)用產(chǎn)生的關(guān)于 thread 'main' 和 RUST_BACKTRACE 的文本。

示例 12-9 展示了為了返回 Result 在 Config::new 的返回值和函數(shù)體中所需的改變。注意這還不能編譯,直到下一個(gè)示例同時(shí)也更新了 main 之后。

文件名: src/main.rs

impl Config {
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

示例 12-9:從 Config::new 中返回 Result

現(xiàn)在 new 函數(shù)返回一個(gè) Result,在成功時(shí)帶有一個(gè) Config 實(shí)例而在出現(xiàn)錯(cuò)誤時(shí)帶有一個(gè) &'static str?;貞浺幌碌谑?“靜態(tài)生命周期” 中講到 &'static str 是字符串字面值的類型,也是目前的錯(cuò)誤信息。

new 函數(shù)體中有兩處修改:當(dāng)沒有足夠參數(shù)時(shí)不再調(diào)用 panic!,而是返回 Err 值。同時(shí)我們將 Config 返回值包裝進(jìn) Ok 成員中。這些修改使得函數(shù)符合其新的類型簽名。

通過讓 Config::new 返回一個(gè) Err 值,這就允許 main 函數(shù)處理 new 函數(shù)返回的 Result 值并在出現(xiàn)錯(cuò)誤的情況更明確的結(jié)束進(jìn)程。

Config::new 調(diào)用并處理錯(cuò)誤

為了處理錯(cuò)誤情況并打印一個(gè)對(duì)用戶友好的信息,我們需要像示例 12-10 那樣更新 main 函數(shù)來處理現(xiàn)在 Config::new 返回的 Result。另外還需要手動(dòng)實(shí)現(xiàn)原先由 panic!負(fù)責(zé)的工作,即以非零錯(cuò)誤碼退出命令行工具的工作。非零的退出狀態(tài)是一個(gè)慣例信號(hào),用來告訴調(diào)用程序的進(jìn)程:該程序以錯(cuò)誤狀態(tài)退出了。

文件名: src/main.rs

use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--

示例 12-10:如果新建 Config 失敗則使用錯(cuò)誤碼退出

在上面的示例中,使用了一個(gè)之前沒有詳細(xì)說明的方法:unwrap_or_else,它定義于標(biāo)準(zhǔn)庫(kù)的 Result<T, E> 上。使用 unwrap_or_else 可以進(jìn)行一些自定義的非 panic! 的錯(cuò)誤處理。當(dāng) Result 是 Ok 時(shí),這個(gè)方法的行為類似于 unwrap:它返回 Ok 內(nèi)部封裝的值。然而,當(dāng)其值是 Err 時(shí),該方法會(huì)調(diào)用一個(gè) 閉包closure),也就是一個(gè)我們定義的作為參數(shù)傳遞給 unwrap_or_else 的匿名函數(shù)。第十三章 會(huì)更詳細(xì)的介紹閉包?,F(xiàn)在你需要理解的是 unwrap_or_else 會(huì)將 Err 的內(nèi)部值,也就是示例 12-9 中增加的 not enough arguments 靜態(tài)字符串的情況,傳遞給閉包中位于兩道豎線間的參數(shù) err。閉包中的代碼在其運(yùn)行時(shí)可以使用這個(gè) err 值。

我們新增了一個(gè) use 行來從標(biāo)準(zhǔn)庫(kù)中導(dǎo)入 process。在錯(cuò)誤的情況閉包中將被運(yùn)行的代碼只有兩行:我們打印出了 err 值,接著調(diào)用了 std::process::exit。process::exit 會(huì)立即停止程序并將傳遞給它的數(shù)字作為退出狀態(tài)碼。這類似于示例 12-8 中使用的基于 panic! 的錯(cuò)誤處理,除了不會(huì)再得到所有的額外輸出了。讓我們?cè)囋嚕?br>

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

非常好!現(xiàn)在輸出對(duì)于用戶來說就友好多了。

從 main 提取邏輯

現(xiàn)在我們完成了配置解析的重構(gòu):讓我們轉(zhuǎn)向程序的邏輯。正如 “二進(jìn)制項(xiàng)目的關(guān)注分離” 部分所展開的討論,我們將提取一個(gè)叫做 run 的函數(shù)來存放目前 main 函數(shù)中不屬于設(shè)置配置或處理錯(cuò)誤的所有邏輯。一旦完成這些,main 函數(shù)將簡(jiǎn)明得足以通過觀察來驗(yàn)證,而我們將能夠?yàn)樗衅渌壿嬀帉憸y(cè)試。

示例 12-11 展示了提取出來的 run 函數(shù)。目前我們只進(jìn)行小的增量式的提取函數(shù)的改進(jìn)。我們?nèi)詫⒃?nbsp;src/main.rs 中定義這個(gè)函數(shù):

文件名: src/main.rs

fn main() {
    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

// --snip--

示例 12-11:提取 run 函數(shù)來包含剩余的程序邏輯

現(xiàn)在 run 函數(shù)包含了 main 中從讀取文件開始的剩余的所有邏輯。run 函數(shù)獲取一個(gè) Config 實(shí)例作為參數(shù)。

從 run 函數(shù)中返回錯(cuò)誤

通過將剩余的邏輯分離進(jìn) run 函數(shù)而不是留在 main 中,就可以像示例 12-9 中的 Config::new 那樣改進(jìn)錯(cuò)誤處理。不再通過 expect 允許程序 panic,run 函數(shù)將會(huì)在出錯(cuò)時(shí)返回一個(gè) Result<T, E>。這讓我們進(jìn)一步以一種對(duì)用戶友好的方式統(tǒng)一 main 中的錯(cuò)誤處理。示例 12-12 展示了 run 簽名和函數(shù)體中的改變:

文件名: src/main.rs

use std::error::Error;

// --snip--

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

示例 12-12:修改 run 函數(shù)返回 Result

這里我們做出了三個(gè)明顯的修改。首先,將 run 函數(shù)的返回類型變?yōu)?nbsp;Result<(), Box<dyn Error>>。之前這個(gè)函數(shù)返回 unit 類型 (),現(xiàn)在它仍然保持作為 Ok 時(shí)的返回值。

對(duì)于錯(cuò)誤類型,使用了 trait 對(duì)象 Box<dyn Error>(在開頭使用了 use 語句將 std::error::Error 引入作用域)。第十七章 會(huì)涉及 trait 對(duì)象。目前只需知道 Box<dyn Error> 意味著函數(shù)會(huì)返回實(shí)現(xiàn)了 Error trait 的類型,不過無需指定具體將會(huì)返回的值的類型。這提供了在不同的錯(cuò)誤場(chǎng)景可能有不同類型的錯(cuò)誤返回值的靈活性。這也就是 dyn,它是 “動(dòng)態(tài)的”(“dynamic”)的縮寫。

第二個(gè)改變是去掉了 expect 調(diào)用并替換為 第九章 講到的 ?。不同于遇到錯(cuò)誤就 panic!? 會(huì)從函數(shù)中返回錯(cuò)誤值并讓調(diào)用者來處理它。

第三個(gè)修改是現(xiàn)在成功時(shí)這個(gè)函數(shù)會(huì)返回一個(gè) Ok 值。因?yàn)?nbsp;run 函數(shù)簽名中聲明成功類型返回值是 (),這意味著需要將 unit 類型值包裝進(jìn) Ok 值中。Ok(()) 一開始看起來有點(diǎn)奇怪,不過這樣使用 () 是慣用的做法,表明調(diào)用 run 函數(shù)只是為了它的副作用;函數(shù)并沒有返回什么有意義的值。

上述代碼能夠編譯,不過會(huì)有一個(gè)警告:

$ cargo run the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust 提示我們的代碼忽略了 Result 值,它可能表明這里存在一個(gè)錯(cuò)誤。但我們卻沒有檢查這里是否有一個(gè)錯(cuò)誤,而編譯器提醒我們這里應(yīng)該有一些錯(cuò)誤處理代碼!現(xiàn)在就讓我們修正這個(gè)問題。

處理 main 中 run 返回的錯(cuò)誤

我們將檢查錯(cuò)誤并使用類似示例 12-10 中 Config::new 處理錯(cuò)誤的技術(shù)來處理他們,不過有一些細(xì)微的不同:

文件名: src/main.rs

fn main() {
    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

我們使用 if let 來檢查 run 是否返回一個(gè) Err 值,不同于 unwrap_or_else,并在出錯(cuò)時(shí)調(diào)用 process::exit(1)。run 并不返回像 Config::new 返回的 Config 實(shí)例那樣需要 unwrap 的值。因?yàn)?nbsp;run 在成功時(shí)返回 (),而我們只關(guān)心檢測(cè)錯(cuò)誤,所以并不需要 unwrap_or_else 來返回未封裝的值,因?yàn)樗粫?huì)是 ()

不過兩個(gè)例子中 if let 和 unwrap_or_else 的函數(shù)體都一樣:打印出錯(cuò)誤并退出。

將代碼拆分到庫(kù) crate

現(xiàn)在我們的 minigrep 項(xiàng)目看起來好多了!現(xiàn)在我們將要拆分 src/main.rs 并將一些代碼放入 src/lib.rs,這樣就能測(cè)試他們并擁有一個(gè)含有更少功能的 main 函數(shù)。

讓我們將所有不是 main 函數(shù)的代碼從 src/main.rs 移動(dòng)到新文件 src/lib.rs 中:

  • ?run? 函數(shù)定義
  • 相關(guān)的 ?use? 語句
  • ?Config? 的定義
  • ?Config::new? 函數(shù)定義

現(xiàn)在 src/lib.rs 的內(nèi)容應(yīng)該看起來像示例 12-13(為了簡(jiǎn)潔省略了函數(shù)體)。注意直到下一個(gè)示例修改完 src/main.rs 之后,代碼還不能編譯:

文件名: src/lib.rs

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

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        // --snip--
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // --snip--
}

示例 12-13:將 Config 和 run 移動(dòng)到 src/lib.rs

這里使用了公有的 pub 關(guān)鍵字:在 Config、其字段和其 new 方法,以及 run 函數(shù)上?,F(xiàn)在我們有了一個(gè)擁有可以測(cè)試的公有 API 的庫(kù) crate 了。

現(xiàn)在需要在 src/main.rs 中將移動(dòng)到 src/lib.rs 的代碼引入二進(jìn)制 crate 的作用域中,如示例 12-14 所示:

文件名: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    // --snip--
    if let Err(e) = minigrep::run(config) {
        // --snip--
    }
}

示例 12-14:將 minigrep crate 引入 src/main.rs 的作用域中

我們添加了一行 use minigrep::Config,它將 Config 類型引入作用域,并使用 crate 名稱作為 run 函數(shù)的前綴。通過這些重構(gòu),所有功能應(yīng)該能夠聯(lián)系在一起并運(yùn)行了。運(yùn)行 cargo run 來確保一切都正確的銜接在一起。

哇哦!我們做了大量的工作,不過我們?yōu)閷淼某晒Υ蛳铝嘶A(chǔ)?,F(xiàn)在處理錯(cuò)誤將更容易,同時(shí)代碼也更加模塊化。從現(xiàn)在開始幾乎所有的工作都將在 src/lib.rs 中進(jìn)行。

讓我們利用這些新創(chuàng)建的模塊的優(yōu)勢(shì)來進(jìn)行一些在舊代碼中難以展開的工作,這些工作在新代碼中非常容易實(shí)現(xiàn),那就是:編寫測(cè)試!

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)