99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

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

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

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

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

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

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

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

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

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

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

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

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

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

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

提取參數(shù)解析器

首先,我們將解析參數(shù)的功能提取到一個 main 將會調(diào)用的函數(shù)中,為將命令行解析邏輯移動到 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)一個 vector,不過不同于在 main 函數(shù)中將索引 1 的參數(shù)值賦值給變量 query 和將索引 2 的值賦值給變量 filename,我們將整個 vector 傳遞給 parse_config 函數(shù)。接著 parse_config 函數(shù)將包含決定哪個參數(shù)該放入哪個變量的邏輯,并將這些值返回到 main。仍然在 main 中創(chuàng)建變量 query 和 filename,不過 main 不再負(fù)責(zé)處理命令行參數(shù)與變量如何對應(yīng)。

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

組合配置值

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

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

注意:一些同學(xué)將這種在復(fù)雜類型更為合適的場景下使用基本類型的反模式稱為 基本類型偏執(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 返回一個 Config 結(jié)構(gòu)體實例

新定義的結(jié)構(gòu)體 Config 中包含字段 query 和 filename。 parse_config 的簽名表明它現(xiàn)在返回一個 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ù),而最簡單但有些不太高效的方式是調(diào)用這些值的 clone 方法。這會生成 Config 實例可以擁有的數(shù)據(jù)的完整拷貝,不過會比儲存字符串?dāng)?shù)據(jù)的引用消耗更多的時間和內(nèi)存。不過拷貝數(shù)據(jù)使得代碼顯得更加直白因為無需管理引用的生命周期,所以在這種情況下犧牲一小部分性能來換取簡潔性的取舍是值得的。

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

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

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

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

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

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

所以現(xiàn)在 parse_config 函數(shù)的目的是創(chuàng)建一個 Config 實例,我們可以將 parse_config 從一個普通函數(shù)變?yōu)橐粋€叫做 new 的與結(jié)構(gòu)體關(guān)聯(lián)的函數(shù)。做出這個改變使得代碼更符合習(xí)慣:可以像標(biāo)準(zhǔn)庫中的 String 調(diào)用 String::new 來創(chuàng)建一個該類型的實例那樣,將 parse_config 變?yōu)橐粋€與 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 并將其移動到 impl 塊中,這使得 new 函數(shù)與 Config 相關(guān)聯(lián)。再次嘗試編譯并確保它可以工作。

修復(fù)錯誤處理

現(xiàn)在我們開始修復(fù)錯誤處理。回憶一下之前提到過如果 args vector 包含少于 3 個項并嘗試訪問 vector 中索引 1 或索引 2 的值會造成程序 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 是一個針對程序員的錯誤信息,然而這并不能真正幫助終端用戶理解發(fā)生了什么和他們應(yīng)該做什么?,F(xiàn)在就讓我們修復(fù)它吧。

改善錯誤信息

在示例 12-8 中,在 new 函數(shù)中增加了一個檢查在訪問索引 1 和 2 之前檢查 slice 是否足夠長。如果 slice 不夠長,我們使用一個更好的錯誤信息 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:增加一個參數(shù)數(shù)量檢查

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

有了 new 中這幾行額外的代碼,再次不帶任何參數(shù)運(yùn)行程序并看看現(xià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 'not enough arguments', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

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

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

示例 12-9 展示了為了返回 Result 在 Config::new 的返回值和函數(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ù)返回一個 Result,在成功時帶有一個 Config 實例而在出現(xiàn)錯誤時帶有一個 &'static str?;貞浺幌碌谑?“靜態(tài)生命周期” 中講到 &'static str 是字符串字面值的類型,也是目前的錯誤信息。

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

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

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

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

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

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

$ 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)在輸出對于用戶來說就友好多了。

從 main 提取邏輯

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

示例 12-11 展示了提取出來的 run 函數(shù)。目前我們只進(jìn)行小的增量式的提取函數(shù)的改進(jìn)。我們?nèi)詫⒃?nbsp;src/main.rs 中定義這個函數(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ù)獲取一個 Config 實例作為參數(shù)。

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

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

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

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

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

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

上述代碼能夠編譯,不過會有一個警告:

$ 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 值,它可能表明這里存在一個錯誤。但我們卻沒有檢查這里是否有一個錯誤,而編譯器提醒我們這里應(yīng)該有一些錯誤處理代碼!現(xiàn)在就讓我們修正這個問題。

處理 main 中 run 返回的錯誤

我們將檢查錯誤并使用類似示例 12-10 中 Config::new 處理錯誤的技術(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 是否返回一個 Err 值,不同于 unwrap_or_else,并在出錯時調(diào)用 process::exit(1)run 并不返回像 Config::new 返回的 Config 實例那樣需要 unwrap 的值。因為 run 在成功時返回 (),而我們只關(guān)心檢測錯誤,所以并不需要 unwrap_or_else 來返回未封裝的值,因為它只會是 ()

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

將代碼拆分到庫 crate

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

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

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

現(xiàn)在 src/lib.rs 的內(nèi)容應(yīng)該看起來像示例 12-13(為了簡潔省略了函數(shù)體)。注意直到下一個示例修改完 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 移動到 src/lib.rs

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

現(xiàn)在需要在 src/main.rs 中將移動到 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)在處理錯誤將更容易,同時代碼也更加模塊化。從現(xiàn)在開始幾乎所有的工作都將在 src/lib.rs 中進(jìn)行。

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號