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)目來解決這些問題。
main
函數(shù)負(fù)責(zé)多個(gè)任務(wù)的組織問題在許多二進(jìn)制項(xiàng)目中很常見。所以 Rust 社區(qū)開發(fā)出一類在 main
函數(shù)開始變得龐大時(shí)進(jìn)行二進(jìn)制程序的關(guān)注分離的指導(dǎo)性過程。這些過程有如下步驟:
經(jīng)過這些過程之后保留在 main
函數(shù)中的責(zé)任應(yīng)該被限制為:
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ù)的功能提取到一個(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)目的的字段名中尋找他們。
目前為止,我們將負(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)。再次嘗試編譯并確保它可以工作。
現(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ù)它吧。
在示例 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。
我們可以選擇返回一個(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)程。
為了處理錯(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ì)于用戶來說就友好多了。
現(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ù)。
通過將剩余的邏輯分離進(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è)問題。
我們將檢查錯(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ò)誤并退出。
現(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ù)定義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è)試!
更多建議: