上一篇:?11-編寫自動化測試
????????本章是對迄今為止所學(xué)到的許多技能的回顧,也是對一些標(biāo)準(zhǔn)庫特性的探索。我們將創(chuàng)建一個(gè)與文件和命令行輸入/輸出交互的命令行工具,以練習(xí)你現(xiàn)在掌握的一些 Rust 概念。
????????Rust 的速度、安全性、單一二進(jìn)制輸出和跨平臺支持使其成為創(chuàng)建命令行工具的理想語言,因此在我們的項(xiàng)目中,我們將制作自己版本的經(jīng)典命令行搜索工具 grep (全局搜索正則表達(dá)式并打?。T谧詈唵蔚氖褂们闆r下, grep 搜索指定文件中的指定字符串。為此, grep 將文件路徑和字符串作為參數(shù)。然后,它讀取文件,查找文件中包含字符串參數(shù)的行,并打印這些行。
????????在此過程中,我們將展示如何讓我們的命令行工具使用許多其他命令行工具所使用的終端功能。我們將讀取環(huán)境變量的值,以便用戶配置工具的行為。我們還將把錯(cuò)誤信息打印到標(biāo)準(zhǔn)錯(cuò)誤控制臺流 ( stderr ) 而不是標(biāo)準(zhǔn)輸出 ( stdout ) 中,這樣,用戶就可以將成功輸出重定向到文件,同時(shí)還能在屏幕上看到錯(cuò)誤信息。
????????一位 Rust 社區(qū)成員 Andrew Gallant 已經(jīng)創(chuàng)建了一個(gè)功能齊全、速度極快的 grep 版本,名為 ripgrep 。相比之下,我們的版本將相當(dāng)簡單,但本章將為你提供一些了解真實(shí)世界項(xiàng)目(如 ripgrep )所需的背景知識。
????????我們的 grep 項(xiàng)目將結(jié)合您目前所學(xué)到的多個(gè)概念:
????????????????①. 組織代碼(使用第 7 章中所學(xué)的模塊知識);
????????????????②. 使用向量和字符串(集合,第 8 章);
????????????????③. 處理錯(cuò)誤 ( 第 9 章 );
????????????????④. 合理使用特質(zhì)和生命周期(第 10 章);
????????????????⑤.?編寫測試 ( 第 11 章 );
????????我們還將簡要介紹閉包、迭代器和特質(zhì)對象,第 13 章和第 17 章將詳細(xì)介紹這些內(nèi)容。
12.1?接受命令行參數(shù)
????????讓我們創(chuàng)建一個(gè)新項(xiàng)目,一如既往地使用 cargo new 。我們將把項(xiàng)目命名為 minigrep ,以區(qū)別于系統(tǒng)中已有的 grep 工具。
cargo.exe new minigrep
Created binary (application) `minigrep` package
????????第一項(xiàng)任務(wù)是讓 minigrep 接受兩個(gè)命令行參數(shù):文件路徑和要搜索的字符串。也就是說,我們希望在運(yùn)行我們的程序時(shí),可以使用 cargo run 、兩個(gè)連字符表示下面的參數(shù)是給我們的程序而不是給 cargo 、一個(gè)要搜索的字符串和一個(gè)要搜索的文件路徑,就像這樣:
$ cargo run -- searchstring example-filename.txt
????????現(xiàn)在, cargo new 生成的程序無法處理我們給它的參數(shù)。crates.io上的一些現(xiàn)有庫可以幫助編寫一個(gè)接受命令行參數(shù)的程序,但因?yàn)槟悴艅傞_始學(xué)習(xí)這個(gè)概念,所以還是讓我們自己來實(shí)現(xiàn)這個(gè)功能吧。
12.1.1?讀取參數(shù)值
????????為了使 minigrep 能夠讀取我們傳遞給它的命令行參數(shù)值,我們需要使用 Rust 標(biāo)準(zhǔn)庫中提供的 std::env::args 函數(shù)。該函數(shù)返回傳給 minigrep 的命令行參數(shù)的迭代器。我們將在第 13 章全面介紹迭代器。現(xiàn)在,你只需要知道關(guān)于迭代器的兩個(gè)細(xì)節(jié):迭代器會產(chǎn)生一系列值,我們可以調(diào)用迭代器上的 collect 方法將其轉(zhuǎn)化為一個(gè)集合,例如向量,其中包含了迭代器產(chǎn)生的所有元素。
????????清單 12-1 中的代碼允許 minigrep 程序讀取傳給它的任何命令行參數(shù),然后將這些值收集到一個(gè)向量中。
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
(清單 12-1:將命令行參數(shù)收集到矢量中并打印出來)
????????首先,我們使用 use 語句將 std::env 模塊引入作用域,這樣就可以使用其 args 函數(shù)。注意 std::env::args 函數(shù)嵌套在兩級模塊中。正如我們在第 7 章中所討論的,在所需函數(shù)嵌套在多個(gè)模塊中的情況下,我們選擇將父模塊而不是函數(shù)引入作用域。通過這種方法,我們可以很容易地使用 std::env 中的其他函數(shù)。這也比添加 use std::env::args ,然后只調(diào)用 args 的做法更容易引起歧義,因?yàn)?args 很容易被誤認(rèn)為是當(dāng)前模塊中定義的函數(shù)。
args 函數(shù)和無效的 Unicode
????????請注意,如果任何參數(shù)包含無效的 Unicode, std::env::args 就會出錯(cuò)。如果您的程序需要接受包含無效 Unicode 的參數(shù),請使用 std::env::args_os 代替。該函數(shù)返回一個(gè)迭代器,生成 OsString 值而不是 String 值。為了簡單起見,我們選擇在此使用 std::env::args ,因?yàn)?OsString 的值因平臺而異,處理起來比 String 的值更復(fù)雜。
????????在 main 的第一行,我們調(diào)用了 env::args ,并立即使用 collect 將迭代器轉(zhuǎn)換為包含迭代器產(chǎn)生的所有值的向量。我們可以使用 collect 函數(shù)創(chuàng)建多種類型的集合,因此我們顯式地注解了 args 的類型,指定我們需要一個(gè)字符串向量。雖然在 Rust 中我們很少需要注解類型,但 collect 是一個(gè)你經(jīng)常需要注解的函數(shù),因?yàn)?Rust 無法推斷出你想要的集合類型。
????????最后,我們使用調(diào)試宏打印向量。讓我們先在沒有參數(shù)的情況下運(yùn)行代碼,然后在有兩個(gè)參數(shù)的情況下運(yùn)行代碼:
cargo.exe run
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.20s
Running `target\debug\minigrep.exe`
[src\main.rs:5] args = [
"target\\debug\\minigrep.exe",
]
cargo.exe run -- needle haystack
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe needle haystack`
[src\main.rs:5] args = [
"target\\debug\\minigrep.exe",
"needle",
"haystack",
]
????????請注意,向量中的第一個(gè)值是 "target/debug/minigrep" ,也就是二進(jìn)制文件的名稱。這與 C 語言參數(shù)列表的行為一致,讓程序在執(zhí)行時(shí)使用調(diào)用的名稱。如果要在信息中打印程序名稱,或者根據(jù)調(diào)用程序時(shí)使用的命令行別名來改變程序的行為,那么訪問程序名稱通常會很方便。但在本章中,我們將忽略它,只保存我們需要的兩個(gè)參數(shù)。
12.1.2?在變量中保存參數(shù)值
????????程序目前可以訪問作為命令行參數(shù)指定的值?,F(xiàn)在,我們需要將這兩個(gè)參數(shù)的值保存到變量中,以便在程序的其余部分中使用這些值。我們將在清單 12-2 中完成這項(xiàng)工作。
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {}", query);
println!("In file {}", file_path);
}
(清單 12-2:創(chuàng)建變量來保存查詢參數(shù)和文件路徑參數(shù))
????????正如我們在打印向量時(shí)看到的那樣,程序名稱占用向量中的第一個(gè)值 args[0] ,因此我們從索引 1 開始輸入?yún)?shù)。 minigrep 的第一個(gè)參數(shù)是我們要搜索的字符串,因此我們在變量 query 中放入對第一個(gè)參數(shù)的引用。第二個(gè)參數(shù)是文件路徑,因此我們將第二個(gè)參數(shù)的引用放入變量 file_path 中。
????????我們暫時(shí)打印這些變量的值,以證明代碼按我們的意圖運(yùn)行。讓我們使用參數(shù) test 和 sample.txt 再次運(yùn)行這個(gè)程序:
cargo.exe run -- test sample.txt
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.72s
Running `target\debug\minigrep.exe test sample.txt`
Searching for test
In file sample.txt
????????很好,程序正在運(yùn)行!我們需要的參數(shù)值被保存到了正確的變量中。稍后,我們將添加一些錯(cuò)誤處理功能,以處理某些潛在的錯(cuò)誤情況,例如用戶沒有提供參數(shù)時(shí);現(xiàn)在,我們將忽略這種情況,轉(zhuǎn)而努力添加文件讀取功能。
12.2?讀取文件
????????現(xiàn)在,我們將添加讀取 file_path 參數(shù)中指定文件的功能。首先,我們需要一個(gè)示例文件來進(jìn)行測試:我們將使用一個(gè)包含多行少量文本和一些重復(fù)單詞的文件。清單 12-3 中有一首艾米莉-狄金森的詩,可以很好地解決這個(gè)問題!在項(xiàng)目根目錄下創(chuàng)建一個(gè)名為 poem.txt 的文件,然后輸入詩歌 "I'm Nobody!你是誰?
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!
(清單 12-3:艾米莉-狄金森的一首詩是一個(gè)很好的測試案例)
????????文本就緒后,編輯 src/main.rs,添加代碼以讀取文件,如清單 12-4 所示。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {}", query);
println!("In file {}", file_path);
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
(清單 12-4:讀取第二個(gè)參數(shù)指定的文件內(nèi)容)
????????首先,我們通過 use 語句引入標(biāo)準(zhǔn)庫的相關(guān)部分:我們需要 std::fs 來處理文件。
????????在 main 中,新語句 fs::read_to_string 接收 file_path ,打開該文件,并返回該文件內(nèi)容的 std::io::Result<String> 。
????????之后,我們再次添加一條臨時(shí) println! 語句,在讀取文件后打印 contents 的值,以便檢查程序是否正常運(yùn)行。
????????讓我們運(yùn)行這段代碼,以任意字符串作為第一個(gè)命令行參數(shù)(因?yàn)槲覀冞€沒有實(shí)現(xiàn)搜索部分),以 poem.txt 文件作為第二個(gè)參數(shù):
cargo.exe run -- the poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe 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!
????????很好!代碼讀取并打印了文件內(nèi)容。但代碼有一些缺陷。目前, main 函數(shù)有多個(gè)職責(zé):一般來說,如果每個(gè)函數(shù)只負(fù)責(zé)一個(gè)想法,那么函數(shù)會更清晰,也更容易維護(hù)。另一個(gè)問題是,我們在處理錯(cuò)誤方面做得不夠好。這個(gè)程序還很小,所以這些缺陷并不是什么大問題,但隨著程序的發(fā)展,要想干凈利落地修復(fù)這些缺陷就會變得更加困難。在開發(fā)程序的早期就開始重構(gòu)是一種很好的做法,因?yàn)橹貥?gòu)少量代碼要容易得多。接下來,我們將進(jìn)行重構(gòu)。
12.3?重構(gòu)以改進(jìn)模塊化和錯(cuò)誤處理
????????為了改進(jìn)我們的程序,我們將修正四個(gè)問題,這些問題與程序的結(jié)構(gòu)和處理潛在錯(cuò)誤的方式有關(guān)。首先,我們的 main 函數(shù)現(xiàn)在執(zhí)行兩項(xiàng)任務(wù):解析參數(shù)和讀取文件。隨著程序的增長, main 函數(shù)處理的獨(dú)立任務(wù)數(shù)量也會增加。當(dāng)一個(gè)函數(shù)的職責(zé)越來越多時(shí),它就會變得更難推理、更難測試、更難在不破壞其中一個(gè)部分的情況下進(jìn)行更改。最好的辦法是將功能分開,讓每個(gè)函數(shù)只負(fù)責(zé)一項(xiàng)任務(wù)。
????????這個(gè)問題也與第二個(gè)問題有關(guān):雖然 query 和 file_path 是我們程序的配置變量,但 contents 等變量用于執(zhí)行程序的邏輯。 main 的長度越長,我們需要納入作用域的變量就越多;納入作用域的變量越多,就越難跟蹤每個(gè)變量的用途。最好將配置變量歸類到一個(gè)結(jié)構(gòu)中,以便明確它們的用途。
????????第三個(gè)問題是,當(dāng)讀取文件失敗時(shí),我們使用 expect 來打印錯(cuò)誤信息,但錯(cuò)誤信息只是打印 Should have been able to read the file 。讀取文件失敗的原因有很多:例如,文件可能丟失,或者我們沒有打開文件的權(quán)限?,F(xiàn)在,無論哪種情況,我們都會打印出相同的錯(cuò)誤信息,這不會給用戶提供任何信息!
????????第四,我們重復(fù)使用 expect 來處理不同的錯(cuò)誤,如果用戶在沒有指定足夠參數(shù)的情況下運(yùn)行我們的程序,他們就會從 Rust 中得到一個(gè) index out of bounds 錯(cuò)誤,而這個(gè)錯(cuò)誤并不能清楚地解釋問題。如果能將所有錯(cuò)誤處理代碼集中在一處,那就再好不過了,這樣,如果錯(cuò)誤處理邏輯需要更改,未來的維護(hù)者只需在一處查閱代碼即可。將所有錯(cuò)誤處理代碼放在一處還能確保我們打印的信息對最終用戶有意義。
????????讓我們通過重構(gòu)項(xiàng)目來解決這四個(gè)問題。
12.3.1?二進(jìn)制項(xiàng)目的關(guān)注點(diǎn)分離
????????將多個(gè)任務(wù)的責(zé)任分配給 main 函數(shù)是許多二進(jìn)制項(xiàng)目的共同組織問題。因此,Rust 社區(qū)制定了指導(dǎo)原則,以便在 main 開始變得龐大時(shí),將二進(jìn)制程序的不同關(guān)注點(diǎn)拆分開來。這一過程包括以下步驟:
????????????????①. 將程序分為 main.rs 和 lib.rs,并將程序邏輯移至 lib.rs。
????????????????②. 只要命令行解析邏輯很小,就可以保留在 main.rs 中。
????????????????③.?當(dāng)命令行解析邏輯開始變得復(fù)雜時(shí),將其從 main.rs 中提取出來并移至 lib.rs。
????????在這一過程結(jié)束后, main ,其職責(zé)應(yīng)限于以下方面:
????????????????①.?使用參數(shù)值調(diào)用命令行解析邏輯
????????????????②.?設(shè)置任何其他配置
? ? ? ? ? ? ? ? ③.?在 lib.rs 中調(diào)用 run 函數(shù)
????????????????④.?如果 run 返回錯(cuò)誤,則進(jìn)行錯(cuò)誤處理
????????這種模式是將關(guān)注點(diǎn)分開:main.rs 處理程序的運(yùn)行,lib.rs 處理手頭任務(wù)的所有邏輯。由于無法直接測試 main 函數(shù),因此這種結(jié)構(gòu)可以將程序的所有邏輯移到 lib.rs 中的函數(shù)中進(jìn)行測試。保留在 main.rs 中的代碼將非常小,足以通過讀取來驗(yàn)證其正確性。讓我們按照這個(gè)過程重新編寫程序。
12.3.1.1?提取參數(shù)解析器
????????我們將把解析參數(shù)的功能提取到 main 將調(diào)用的函數(shù)中,以便將命令行解析邏輯移至 src/lib.rs。清單 12-5 顯示了 main 的新開頭,它調(diào)用了一個(gè)新函數(shù) parse_config ,我們將暫時(shí)在 src/main.rs 中定義該函數(shù)。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
(清單 12-5:從 parse_config 中提取一個(gè)函數(shù) main)
????????我們?nèi)匀粚⒚钚袇?shù)收集到一個(gè)向量中,但在 main 函數(shù)中,我們不再將索引 1 的參數(shù)值賦值給變量 query ,也不再將索引 2 的參數(shù)值賦值給變量 file_path ,而是將整個(gè)向量傳遞給 parse_config 函數(shù)。然后, parse_config 函數(shù)將負(fù)責(zé)確定哪個(gè)參數(shù)放在哪個(gè)變量中,并將值傳回 main 。我們?nèi)匀辉?main 中創(chuàng)建 query 和 file_path 變量,但 main 不再負(fù)責(zé)確定命令行參數(shù)和變量的對應(yīng)關(guān)系。
????????對于我們的小程序來說,這樣的重構(gòu)似乎有些矯枉過正,但我們是在以小步、漸進(jìn)的方式進(jìn)行重構(gòu)。完成這一改動后,再次運(yùn)行程序,驗(yàn)證參數(shù)解析是否仍然有效。經(jīng)常檢查進(jìn)度很有好處,有助于在出現(xiàn)問題時(shí)找出原因。
12.3.1.2?分組配置值
????????我們還可以進(jìn)一步改進(jìn) parse_config 函數(shù)。目前,我們返回的是一個(gè)元組,但緊接著我們又將這個(gè)元組分解成了單獨(dú)的部分。這說明我們的抽象方法可能還不夠正確。
????????parse_config 的 config 部分是另一個(gè)有待改進(jìn)的指標(biāo),它意味著我們返回的兩個(gè)值是相關(guān)的,都是一個(gè)配置值的一部分。目前,除了將兩個(gè)值組合成一個(gè)元組之外,我們并沒有在數(shù)據(jù)結(jié)構(gòu)中傳達(dá)這種含義;相反,我們將把這兩個(gè)值放入一個(gè)結(jié)構(gòu)體中,并給結(jié)構(gòu)體的每個(gè)字段起一個(gè)有意義的名字。這樣做可以讓將來的代碼維護(hù)者更容易理解不同值之間的關(guān)系以及它們的用途。
????????清單 12-6 顯示了對 parse_config 函數(shù)的改進(jìn)。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
(清單 12-6:重構(gòu) parse_config 以返回 Config 結(jié)構(gòu)體的實(shí)例)
????????我們添加了一個(gè)名為 Config 的結(jié)構(gòu)體,該結(jié)構(gòu)體定義了名為 query 和 file_path 的字段。 parse_config 的簽名現(xiàn)在表示它返回一個(gè) Config 值。在 parse_config 的主體中,我們過去返回的是引用 args 中 String 值的字符串片段,現(xiàn)在我們定義 Config 為包含 String 值的所有者。 main 中的 args 變量是參數(shù)值的所有者,它只允許 parse_config 函數(shù)借用這些值,這意味著如果 Config 試圖占有 args 中的值,我們將違反 Rust 的借用規(guī)則。
????????我們可以通過多種方法管理 String 數(shù)據(jù);最簡單的方法是在值上調(diào)用 clone 方法,雖然效率有點(diǎn)低。這將為 Config 實(shí)例制作一份完整的數(shù)據(jù)副本,這比存儲字符串?dāng)?shù)據(jù)的引用耗費(fèi)更多時(shí)間和內(nèi)存。不過,克隆數(shù)據(jù)也使我們的代碼變得非常簡單,因?yàn)槲覀儾槐毓芾硪玫纳芷冢?u>在這種情況下,為了獲得簡單性而犧牲一點(diǎn)性能是值得的。
使用的利弊得失 clone
????????許多 Rustaceans 都傾向于避免使用 clone 來解決所有權(quán)問題,因?yàn)樗倪\(yùn)行成本很高。在第 13 章中,你將學(xué)習(xí)如何在這種情況下使用更有效的方法。但現(xiàn)在,復(fù)制幾個(gè)字符串以繼續(xù)取得進(jìn)展是沒有問題的,因?yàn)槟阒恍鑿?fù)制一次,而且文件路徑和查詢字符串都非常小。有一個(gè)效率有點(diǎn)低的工作程序,總比第一次就試圖對代碼進(jìn)行超優(yōu)化要好。隨著你對 Rust 的使用經(jīng)驗(yàn)越來越豐富,從最高效的解決方案開始會變得更容易,但現(xiàn)在,調(diào)用 clone .
????????我們更新了 main ,將 parse_config 返回的 Config 實(shí)例放入一個(gè)名為 config 的變量中,并更新了之前使用單獨(dú) query 和 file_path 變量的代碼,現(xiàn)在改用 Config 結(jié)構(gòu)上的字段。
????????現(xiàn)在,我們的代碼更清楚地表明 query 和 file_path 是相關(guān)的,它們的目的是配置程序的工作方式。任何使用這些值的代碼都知道要在 config 實(shí)例中根據(jù)其目的命名的字段中找到它們。
12.3.1.3 為 Config創(chuàng)建構(gòu)造
????????到目前為止,我們已經(jīng)從 main 中提取了負(fù)責(zé)解析命令行參數(shù)的邏輯,并將其置于 parse_config 函數(shù)中。這樣做讓我們明白, query 和 file_path 的值是相關(guān)的,這種關(guān)系應(yīng)該在我們的代碼中傳達(dá)出來。然后,我們添加了一個(gè) Config 結(jié)構(gòu),用于命名 query 和 file_path 的相關(guān)目的,并能夠從 parse_config 函數(shù)中以結(jié)構(gòu)字段名稱的形式返回值的名稱。
????????因此,既然 parse_config 函數(shù)的目的是創(chuàng)建 Config 實(shí)例,我們就可以將 parse_config 從一個(gè)普通函數(shù)改為與 Config 結(jié)構(gòu)相關(guān)聯(lián)的名為 new 的函數(shù)。這一改動將使代碼更加習(xí)以為常。我們可以通過調(diào)用 String::new 來創(chuàng)建標(biāo)準(zhǔn)庫中類型的實(shí)例,如 String 。同樣,通過將 parse_config 改為與 Config 關(guān)聯(lián)的 new 函數(shù),我們就可以通過調(diào)用 Config::new 來創(chuàng)建 Config 的實(shí)例。清單 12-7 顯示了我們需要進(jìn)行的更改。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
(清單 12-7:將 parse_config 改為 Config::new)
????????我們更新了 main ,將原來調(diào)用 parse_config 的地方改為調(diào)用 Config::new 。我們將 parse_config 更名為 new ,并將其移至 impl 代碼塊中,該代碼塊將 new 函數(shù)與 Config 關(guān)聯(lián)。請?jiān)俅螄L試編譯這段代碼,以確保它能正常工作。
12.3.2?修復(fù)錯(cuò)誤處理
????????現(xiàn)在我們來修正錯(cuò)誤處理?;叵胍幌拢绻噲D訪問 args 向量中索引 1 或索引 2 的值,而該向量包含的項(xiàng)目少于三個(gè),程序就會崩潰。試著在不帶任何參數(shù)的情況下運(yùn)行程序,結(jié)果會是這樣的:
cargo.exe run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe`
thread 'main' panicked at src\main.rs:25:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 101)
????????index out of bounds: the len is 1 but the index is 1 行是針對程序員的錯(cuò)誤信息。它無法幫助我們的最終用戶理解他們應(yīng)該做什么。讓我們現(xiàn)在就解決這個(gè)問題。
12.3.2.1 改進(jìn)錯(cuò)誤信息
????????在清單 12-8 中,我們在 new 函數(shù)中添加了一個(gè)檢查,在訪問索引 1 和索引 2 之前,該檢查將驗(yàn)證切片是否足夠長。如果片段不夠長,程序就會驚慌失措,并顯示更好的錯(cuò)誤信息。
impl Config {
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
(清單 12-8:添加參數(shù)個(gè)數(shù)檢查)
????????這段代碼與清單 9-13 中編寫的 Guess::new 函數(shù)類似,當(dāng) value 參數(shù)超出有效值范圍時(shí),我們調(diào)用了 panic! 。在這里,我們不是檢查值的范圍,而是檢查 args 的長度是否至少為 3,函數(shù)的其余部分可以在滿足這一條件的前提下運(yùn)行。如果 args 的項(xiàng)目少于 3 個(gè),則該條件為真,我們將調(diào)用 panic! 宏立即結(jié)束程序。
????????有了這些額外的幾行代碼 new ,讓我們再次不帶任何參數(shù)運(yùn)行程序,看看現(xiàn)在的錯(cuò)誤是什么樣子的:
cargo.exe run
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.49s
Running `target\debug\minigrep.exe`
thread 'main' panicked at src\main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 101)
????????這樣的輸出效果更好:我們現(xiàn)在有了一條合理的錯(cuò)誤信息。不過,我們也有了不想提供給用戶的無關(guān)信息。也許我們在清單 9-13 中使用的技術(shù)并不適合在這里使用:調(diào)用 panic! 更適合編程問題,而不是使用問題,這在第 9 章中已經(jīng)討論過。相反,我們將使用在第 9 章中學(xué)到的另一種技巧--返回一個(gè)表示成功或錯(cuò)誤的 Result 。
12.3.2.2 返回 Result 而不是呼叫 panic!
????????相反,我們可以返回一個(gè) Result 值,在成功的情況下,它將包含一個(gè) Config 實(shí)例,而在錯(cuò)誤的情況下,它將描述問題所在。我們還要將函數(shù)名稱從 new 改為 build ,因?yàn)樵S多程序員希望 new 函數(shù)永遠(yuǎn)不會失敗。當(dāng) Config::build 與 main 通信時(shí),我們可以使用 Result 類型來提示出現(xiàn)了問題。然后,我們可以修改 main ,將 Err 變體轉(zhuǎn)換為對用戶來說更實(shí)用的錯(cuò)誤,而不會像調(diào)用 panic! 時(shí)那樣出現(xiàn)關(guān)于 thread 'main' 和 RUST_BACKTRACE 的周圍文字。
????????清單 12-9 顯示了我們需要對現(xiàn)在調(diào)用的函數(shù) Config::build 的返回值以及返回 Result 所需的函數(shù)體進(jìn)行的修改。需要注意的是,在我們更新 main 之前,這將無法編譯,我們將在下一個(gè)清單中進(jìn)行更新。
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
(清單 12-9:從 Result 返回 Config::build)
????????我們的 build 函數(shù)會返回一個(gè) Result ,成功時(shí)會返回一個(gè) Config 實(shí)例,出錯(cuò)時(shí)會返回一個(gè) &'static str 。我們的錯(cuò)誤值將始終是具有 'static 生命周期的字符串文字。
????????我們在函數(shù)體中做了兩處改動:當(dāng)用戶沒有傳入足夠的參數(shù)時(shí),我們不再調(diào)用 panic! ,而是返回一個(gè) Err 值;我們將 Config 返回值封裝在 Ok 中。這些更改使函數(shù)符合其新的類型簽名。
????????從 Config::build 返回 Err 值可讓 main 函數(shù)處理從 build 函數(shù)返回的 Result 值,并在出錯(cuò)情況下更干凈利落地退出進(jìn)程。
12.3.2.3 調(diào)用 Config::build 和處理錯(cuò)誤
????????為了處理錯(cuò)誤情況并打印用戶友好的信息,我們需要更新 main 以處理 Config::build 返回的 Result ,如清單 12-10 所示。我們還將從 panic! 接管以非零錯(cuò)誤代碼退出命令行工具的職責(zé),轉(zhuǎn)而由人工來實(shí)現(xiàn)。非零退出狀態(tài)是向調(diào)用我們程序的進(jìn)程發(fā)出信號,表明程序以錯(cuò)誤狀態(tài)退出的約定。
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
(清單 12-10:如果構(gòu)建 Config 失敗,則以錯(cuò)誤代碼退出)
????????在本列表中,我們使用了一種尚未詳細(xì)介紹的方法: unwrap_or_else 該方法由標(biāo)準(zhǔn)庫定義于 Result<T, E> 。使用 unwrap_or_else 允許我們定義一些自定義的、非 panic! 的錯(cuò)誤處理。如果 Result 是 Ok 值,該方法的行為與 unwrap 類似:它返回 Ok 封裝的內(nèi)部值。但是,如果該值是 Err 值,該方法將調(diào)用閉包中的代碼,閉包是我們定義的匿名函數(shù),并作為參數(shù)傳遞給 unwrap_or_else (類似C++中的Lambda表達(dá)式)。我們將在第 13 章詳細(xì)介紹閉包?,F(xiàn)在,您只需知道 unwrap_or_else 將把 Err 的內(nèi)值(在本例中是我們在清單 12-9 中添加的靜態(tài)字符串 "not enough arguments" )傳遞給閉包,參數(shù) err 出現(xiàn)在垂直管道之間。然后,閉包中的代碼就可以在運(yùn)行時(shí)使用 err 的值。
????????我們添加了新的 use 行,以便將標(biāo)準(zhǔn)庫中的 process 引入作用域。在錯(cuò)誤情況下運(yùn)行的閉包代碼只有兩行:我們打印 err 值,然后調(diào)用 process::exit 。 process::exit 函數(shù)將立即停止程序,并返回作為退出狀態(tài)代碼傳遞的數(shù)字。這與清單 12-8 中使用的基于 panic! 的處理方法類似,但我們不再獲得所有額外的輸出。讓我們試試看:
cargo.exe run
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target\debug\minigrep.exe`
Problem parsing arguments: not enough arguments
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)
????????很好!這樣的輸出對用戶來說更友好了。
12.3.3 從邏輯中提取 main
????????現(xiàn)在,我們已經(jīng)完成了配置解析的重構(gòu),下面我們來看看程序的邏輯。正如我們在 "二進(jìn)制項(xiàng)目的關(guān)注點(diǎn)分離 "中所述,我們將提取一個(gè)名為 run 的函數(shù),該函數(shù)將保存當(dāng)前 main 函數(shù)中不涉及配置設(shè)置或錯(cuò)誤處理的所有邏輯。完成后, main 將變得簡潔并易于通過檢查進(jìn)行驗(yàn)證,我們也可以為所有其他邏輯編寫測試。
????????清單 12-11 顯示了提取后的 run 函數(shù)。目前,我們只是對提取函數(shù)進(jìn)行了小規(guī)模的漸進(jìn)式改進(jìn)。我們?nèi)栽?src/main.rs 中定義函數(shù)。
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
(清單 12-11:提取包含程序邏輯其余部分的 run 函數(shù))
????????run 函數(shù)現(xiàn)在包含 main 的所有剩余邏輯,從讀取文件開始。 run 函數(shù)將 Config 實(shí)例作為參數(shù)。
12.3.3.1?從 run 函數(shù)返回錯(cuò)誤信息
????????將剩余的程序邏輯分離到 run 函數(shù)后,我們可以改進(jìn)錯(cuò)誤處理,就像清單 12-9 中對 Config::build 所做的那樣。當(dāng)出現(xiàn)錯(cuò)誤時(shí), run 函數(shù)將返回 Result<T, E> ,而不是通過調(diào)用 expect 讓程序驚慌失措。這樣,我們就能以用戶友好的方式將處理錯(cuò)誤的邏輯進(jìn)一步整合到 main 中。清單 12-12 顯示了我們需要對 run 的簽名和主體進(jìn)行的修改。
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
(清單 12-12:更改 run 函數(shù)以返回 Result)
????????我們在這里做了三處重大改動。首先,我們將 run 函數(shù)的返回類型改為 Result<(), Box<dyn Error>> 。該函數(shù)之前返回的是單位類型 () ,我們將其保留為 Ok 的返回值。
????????對于錯(cuò)誤類型,我們使用了特質(zhì)對象 Box<dyn Error> (我們通過頂部的 use 語句將 std::error::Error 帶入了作用域)。我們將在第 17 章介紹特質(zhì)對象?,F(xiàn)在,我們只需知道 Box<dyn Error> 意味著函數(shù)將返回一個(gè)實(shí)現(xiàn)了 Error 特質(zhì)的類型,但我們不必指定返回值的具體類型。這讓我們可以靈活地在不同的錯(cuò)誤情況下返回不同類型的錯(cuò)誤值。 dyn 關(guān)鍵字是 "動態(tài) "的簡稱。
????????其次,我們刪除了對 expect 的調(diào)用,轉(zhuǎn)而使用 ? 操作符,正如我們在第 9 章中提到的那樣。在出現(xiàn)錯(cuò)誤時(shí), ? 將返回當(dāng)前函數(shù)的錯(cuò)誤值供調(diào)用者處理,而不是 panic! 。
????????第三,在成功情況下, run 函數(shù)現(xiàn)在返回一個(gè) Ok 值。我們在簽名中將 run 函數(shù)的成功類型聲明為 () ,這意味著我們需要在 Ok 值中封裝單元類型值。這種 Ok(()) 語法初看起來可能有點(diǎn)奇怪,但像這樣使用 () 是一種慣用的方式,表示我們調(diào)用 run 只是為了它的副作用;它并不返回我們需要的值。
????????運(yùn)行這段代碼時(shí),會編譯成功,但會顯示警告:
cargo.exe run the .\poem.txt
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
warning: unused `Result` that must be used
--> src\main.rs:17:5
|
17 | run(config);
| ^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
17 | let _ = run(config);
| +++++++
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.66s
Running `target\debug\minigrep.exe 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!
12.3.3.2 處理 run 中返回的錯(cuò)誤 main
????????我們將使用類似于清單 12-10 中 Config::build 的技術(shù)來檢查錯(cuò)誤并處理錯(cuò)誤,但兩者略有不同:
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
????????我們使用 if let 而不是 unwrap_or_else 來檢查 run 是否返回 Err 值,如果返回,則調(diào)用 process::exit(1) 。 run 函數(shù)不會像 Config::build 返回 Config 實(shí)例那樣返回我們希望 unwrap 返回的值。由于 run 在成功情況下返回 () ,我們只關(guān)心是否檢測到錯(cuò)誤,因此我們不需要 unwrap_or_else 返回未封裝的值,因?yàn)樗粫祷?() 。
????????if let 和 unwrap_or_else 函數(shù)的主體在兩種情況下都是一樣的:我們打印錯(cuò)誤并退出。
12.3.4 將代碼拆分成一個(gè)crate
????????到目前為止,我們的 minigrep 項(xiàng)目看起來還不錯(cuò)!現(xiàn)在,我們將拆分 src/main.rs 文件,把一些代碼放到 src/lib.rs 文件中。這樣我們就可以測試代碼,并減少 src/main.rs 文件的責(zé)任。
????????讓我們把所有不是 main 函數(shù)的代碼從 src/main.rs 移到 src/lib.rs:
????????????????①. run 函數(shù)定義;
????????????????②. use 相關(guān)聲明;
????????????????③. Config的定義;
????????????????④.?Config::build 函數(shù)定義;
????????src/lib.rs 的內(nèi)容應(yīng)具有清單 12-13 所示的簽名(為簡潔起見,我們省略了函數(shù)體)。請注意,在修改清單 12-14 中的 src/main.rs 之前,該代碼無法編譯。
use std::fs;
use std::error::Error;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
(清單 12-13:將 Config 和 run 移入 src/lib.rs)
????????我們在 Config 、其字段和 build 方法以及 run 函數(shù)中大量使用了 pub 關(guān)鍵字。現(xiàn)在我們有了一個(gè)庫crate,它有一個(gè)我們可以測試的公共 API!
????????現(xiàn)在,我們需要將移至 src/lib.rs 的代碼引入 src/main.rs 中二進(jìn)制 crate 的作用域,如清單 12-14 所示。
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = minigrep::run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
(清單 12-14:在 src/main.rs 中使用 minigrep 庫 crate)
????????我們添加了 use minigrep::Config 行,將庫crate中的 Config 類型引入二進(jìn)制crate的作用域,并在 run 函數(shù)前加上我們的crate名稱?,F(xiàn)在,所有功能都應(yīng)連接起來并能正常工作。使用 cargo run 運(yùn)行程序,確保一切運(yùn)行正常。
????????呼!雖然工作量很大,但我們已經(jīng)為將來的成功做好了準(zhǔn)備?,F(xiàn)在處理錯(cuò)誤要容易得多,而且代碼也更加模塊化了。從現(xiàn)在起,我們幾乎所有的工作都將在 src/lib.rs 中完成。
????????讓我們利用這一新發(fā)現(xiàn)的模塊化優(yōu)勢,做一些在舊代碼中很困難,但在新代碼中卻很容易的事情:我們來寫一些測試!
12.4?使用測試驅(qū)動開發(fā)方法開發(fā)庫文件
????????現(xiàn)在,我們已將邏輯提取到 src/lib.rs,而將參數(shù)收集和錯(cuò)誤處理留在了 src/main.rs,這樣編寫代碼核心功能測試就容易多了。我們可以直接使用各種參數(shù)調(diào)用函數(shù)并檢查返回值,而不必從命令行調(diào)用二進(jìn)制文件。
????????在本節(jié)中,我們將使用測試驅(qū)動開發(fā)(TDD)流程在 minigrep 程序中添加搜索邏輯,具體步驟如下:
????????????????①. 編寫一個(gè)失敗的測試并運(yùn)行它,以確保失敗的原因是你所期望的。
????????????????②. 編寫或修改足夠的代碼,使新測試通過。
????????????????③. 重構(gòu)剛剛添加或更改的代碼,確保測試?yán)^續(xù)通過。
????????????????④.?重復(fù)步驟 1!
什么是測試驅(qū)動開發(fā) ?
????????測試驅(qū)動開發(fā)(TDD, test-driven development)是一種軟件開發(fā)方法,即在實(shí)際代碼執(zhí)行之前編寫測試。它是一種用于確保所開發(fā)的軟件滿足指定要求的技術(shù),并有助于在開發(fā)周期的早期捕捉錯(cuò)誤。該過程遵循一個(gè)短暫的重復(fù)周期,Red-Green-Refactor,即 "紅-綠-重構(gòu)":
? ? ? ? ①. 紅色編寫一個(gè)定義函數(shù)或函數(shù)改進(jìn)的測試,該測試最初失敗的原因應(yīng)該是函數(shù)不存在或不按預(yù)期執(zhí)行。
? ? ? ? ②. 綠色:編寫通過測試所需的最少代碼量。這一階段的主要目標(biāo)是在遵守指定要求的前提下盡快通過測試。
? ? ? ? ③. 重構(gòu):測試通過后,下一步就是重構(gòu)代碼。這包括清理新添加的代碼,改進(jìn)其結(jié)構(gòu)和可讀性,但不改變其行為。重構(gòu)也是為了確保代碼符合良好的設(shè)計(jì)原則和編碼標(biāo)準(zhǔn)。
????????TDD 的好處包括提高代碼質(zhì)量、改進(jìn)設(shè)計(jì)、使代碼庫更易于維護(hù)和更新。它還鼓勵(lì)開發(fā)人員在編寫代碼前對設(shè)計(jì)和需求進(jìn)行深入思考,從而獲得更周到、更高效的解決方案。此外,由于測試是先編寫的,因此 TDD 可以產(chǎn)生一套全面的測試,為代碼提供文檔,并在將來出現(xiàn)問題時(shí)更容易識別。
????????雖然 TDD 只是編寫軟件的眾多方法之一,但它有助于推動代碼設(shè)計(jì)。在編寫使測試通過的代碼之前編寫測試,有助于在整個(gè)過程中保持較高的測試覆蓋率。
????????我們將試運(yùn)行功能的實(shí)現(xiàn),該功能將在文件內(nèi)容中實(shí)際搜索查詢字符串,并生成與查詢匹配的行列表。我們將在名為 search 的函數(shù)中添加這一功能。
12.4.1 編寫失敗的測試
????????由于不再需要這些語句,我們將刪除 src/lib.rs 和 src/main.rs 中用于檢查程序行為的 println! 語句(12.6章節(jié)還需要用此打印)。然后,在 src/lib.rs 中添加一個(gè)帶有測試函數(shù)的 tests 模塊,就像我們在第 11 章中所做的那樣。測試函數(shù)指定了我們希望 search 函數(shù)具有的行為:它將接受一個(gè)查詢和要搜索的文本,并只返回文本中包含查詢的行。清單 12-15 顯示了這個(gè)測試,它還不能編譯。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
(清單 12-15:為我們希望擁有的 search 函數(shù)創(chuàng)建失敗測試)
????????此測試搜索字符串 "duct" 。我們要搜索的文本有三行,其中只有一行包含 "duct" (請注意,開頭雙引號后的反斜杠告訴 Rust 不要在該字符串字面內(nèi)容的開頭添加換行符)。我們斷言從 search 函數(shù)返回的值只包含我們期望的那一行。
????????我們還不能運(yùn)行這個(gè)測試并觀察它是否失敗,因?yàn)闇y試甚至沒有編譯: search 函數(shù)還不存在!根據(jù) TDD 原則,我們只需添加足夠的代碼就能讓測試編譯和運(yùn)行,方法是添加 search 函數(shù)的定義,該函數(shù)總是返回空向量,如清單 12-16 所示。然后,測試應(yīng)該會編譯失敗,因?yàn)榭障蛄颗c包含以下行的向量不匹配 "safe, fast, productive."
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
(清單 12-16:只需定義 search 函數(shù)的足夠部分,我們的測試就可以編譯了)
????????請注意,我們需要在 search 的簽名中定義一個(gè)明確的生命周期 'a ,并在 contents 參數(shù)和返回值中使用該生命周期?;仡櫟?10 章,生命周期參數(shù)指定了哪個(gè)參數(shù)的生命周期與返回值的生命周期相關(guān)聯(lián)。在這種情況下,我們指出返回向量應(yīng)包含引用參數(shù) contents 的字符串片段(而不是參數(shù) query )。
????????換句話說,我們告訴 Rust, search 函數(shù)返回的數(shù)據(jù)將與在 contents 參數(shù)中傳入 search 函數(shù)的數(shù)據(jù)一樣長。這一點(diǎn)很重要!如果編譯器認(rèn)為我們是在制作 query 而不是 contents 的字符串切片,那么它就會錯(cuò)誤地進(jìn)行安全檢查。
????????如果我們忘記了 lifetime 注釋,并嘗試編譯此函數(shù),就會出現(xiàn)此錯(cuò)誤:
cargo.exe build
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
error[E0106]: missing lifetime specifier
--> src\lib.rs:44:51
|
44 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
44 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to previous error
????????Rust 不可能知道我們需要這兩個(gè)參數(shù)中的哪一個(gè),因此我們需要明確地告訴它。因?yàn)?contents 是包含所有文本的參數(shù),而我們希望返回文本中匹配的部分,所以我們知道 contents 是應(yīng)該使用 lifetime 語法連接到返回值的參數(shù)。
????????其他編程語言并不要求在簽名中將參數(shù)與返回值連接起來,但隨著時(shí)間的推移,這種做法會變得越來越簡單。您可以將本示例與第 10 章中的 "使用生命周期驗(yàn)證引用 "部分進(jìn)行比較。
????????現(xiàn)在讓我們運(yùn)行測試:
cargo.exe test
test tests::one_result ... FAILED
failures:
---- tests::one_result stdout ----
thread 'tests::one_result' panicked at src\lib.rs:40:9:
assertion `left == right` failed
left: ["safe, fast, productive."]
right: []
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
????????很好,測試失敗了,和我們預(yù)想的一樣。讓我們讓測試通過吧!
12.4.2 編寫通過測試的代碼
????????目前,我們的測試失敗是因?yàn)槲覀兛偸欠祷匾粋€(gè)空向量。要解決這個(gè)問題并實(shí)現(xiàn) search ,我們的程序需要遵循以下步驟:
????????????????①.?迭代內(nèi)容的每一行。
????????????????②.?檢查該行是否包含我們的查詢字符串。
????????????????③. 如果有,則將其添加到我們返回的值列表中。
????????????????④. 如果沒有,那就什么也別做。
????????????????⑤.?返回匹配的結(jié)果列表。
????????讓我們從迭代各行開始,逐一完成每個(gè)步驟。
12.4.2.1 使用 lines 方法迭代每行
????????Rust 提供了一種有用的方法來處理字符串的逐行迭代,命名為 lines ,如清單 12-17 所示。注意,這個(gè)方法還不能編譯。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
(清單 12-17:遍歷 contents)
????????lines 方法返回一個(gè)迭代器。我們將在第 13 章深入討論迭代器,但請記住,您在清單 3-5 中看到過這種使用迭代器的方法,在清單 3-5 中,我們使用帶有迭代器的 for 循環(huán)對集合中的每個(gè)項(xiàng)運(yùn)行一些代碼。
12.4.2.2 搜索查詢每一行
????????接下來,我們要檢查當(dāng)前行是否包含我們的查詢字符串。幸運(yùn)的是,字符串中有一個(gè)名為 contains 的有用方法可以幫我們做到這一點(diǎn)!在 search 函數(shù)中添加對 contains 方法的調(diào)用,如清單 12-18 所示。請注意,這仍然無法編譯。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
(清單 12-18:添加功能以查看該行是否包含以下字符串 query)
????????目前,我們正在構(gòu)建功能。為了使其能夠編譯,我們需要從主體中返回一個(gè)值,就像我們在函數(shù)簽名中指出的那樣。
12.4.2.3 存儲匹配行
????????要完成這個(gè)函數(shù),我們需要一種方法來存儲想要返回的匹配行。為此,我們可以在 for 循環(huán)之前創(chuàng)建一個(gè)可變向量,并調(diào)用 push 方法在向量中存儲 line 。在 for 循環(huán)之后,我們返回向量,如清單 12-19 所示。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
(清單 12-19:存儲匹配的行,以便返回)
????????現(xiàn)在 search 函數(shù)應(yīng)該只返回包含 query 的行,我們的測試應(yīng)該會通過。讓我們運(yùn)行測試:
cargo.exe test
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src\main.rs (target\debug\deps\minigrep-9cd602df4200fb7d.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
????????測試通過了,所以我們知道它能正常工作!
????????此時(shí),我們可以考慮重構(gòu)搜索函數(shù)的實(shí)現(xiàn),同時(shí)保持測試通過,以維持相同的功能。搜索函數(shù)中的代碼不算太差,但它沒有利用到迭代器的一些有用特性。我們將在第 13 章回到這個(gè)示例,詳細(xì)探討迭代器,并研究如何改進(jìn)它。
12.4.3 在 run 函數(shù)中使用 search 函數(shù)
????????現(xiàn)在 search 函數(shù)已經(jīng)正常工作并經(jīng)過測試,我們需要從 run 函數(shù)中調(diào)用 search 。我們需要將 config.query 值和 run 從文件中讀取的 contents 傳遞給 search 函數(shù)。然后, run 將打印從 search 返回的每一行:
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
????????我們?nèi)匀皇褂?for 循環(huán)從 search 返回每一行并打印出來。
????????現(xiàn)在整個(gè)程序應(yīng)該可以運(yùn)行了!讓我們試試看,首先用一個(gè)單詞,它應(yīng)該能準(zhǔn)確返回艾米莉-狄金森的詩歌 "frog?"中的一行:
cargo.exe run -- frog .\poem.txt
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.65s
Running `target\debug\minigrep.exe frog .\poem.txt`
Searching for frog
In file .\poem.txt
How public, like a frog
????????酷!現(xiàn)在,讓我們試試能匹配多行的單詞,比如 "body":
cargo.exe run -- body .\poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe body .\poem.txt`
Searching for body
In file .\poem.txt
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
????????最后,讓我們確保在搜索詩歌中沒有的單詞時(shí),不會得到任何詩行,比如 "monomorphization":
cargo.exe run -- monomorphization .\poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe monomorphization .\poem.txt`
Searching for monomorphization
In file .\poem.txt
????????好極了我們建立了自己的迷你版經(jīng)典工具,學(xué)到了很多關(guān)于如何構(gòu)建應(yīng)用程序的知識。我們還學(xué)到了一些關(guān)于文件輸入和輸出、生命周期、測試和命令行解析的知識。
????????為了完善這個(gè)項(xiàng)目,我們將簡要演示如何使用環(huán)境變量和如何打印到標(biāo)準(zhǔn)錯(cuò)誤,這兩樣?xùn)|西在編寫命令行程序時(shí)都很有用。
12.5?使用環(huán)境變量
????????我們將通過添加一項(xiàng)額外功能來改進(jìn) minigrep :用戶可通過環(huán)境變量打開大小寫不敏感搜索選項(xiàng)。我們可以將此功能設(shè)置為命令行選項(xiàng),要求用戶每次使用時(shí)都輸入該選項(xiàng),但將其設(shè)置為環(huán)境變量后,用戶只需設(shè)置一次環(huán)境變量,就可以在該終端會話中進(jìn)行大小寫不敏感搜索。
12.5.1?為大小寫敏感的 search 函數(shù)編寫失敗測試
????????我們首先添加一個(gè)新的 search_case_insensitive 函數(shù),該函數(shù)將在環(huán)境變量有值時(shí)被調(diào)用。我們將繼續(xù)遵循 TDD 流程,因此第一步還是要編寫一個(gè)失敗測試。我們將為新函數(shù) search_case_insensitive 添加一個(gè)新測試,并將舊測試從 one_result 重命名為 case_sensitive ,以明確兩個(gè)測試之間的區(qū)別,如清單 12-20 所示。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
(清單 12-20:為我們將要添加的大小寫不敏感函數(shù)添加一個(gè)新的失敗測試)
????????請注意,我們也編輯了舊測試的 contents 。我們添加了一行新的文本 "Duct tape." ,其中使用了大寫字母 D,當(dāng)我們以區(qū)分大小寫的方式進(jìn)行搜索時(shí),它不應(yīng)該與查詢 "duct" 匹配。以這種方式更改舊測試有助于確保我們不會意外破壞已經(jīng)實(shí)現(xiàn)的大小寫敏感搜索功能。這個(gè)測試現(xiàn)在應(yīng)該可以通過,而且在我們進(jìn)行大小寫不敏感搜索時(shí)也會繼續(xù)通過。
????????不區(qū)分大小寫搜索的新測試使用 "rUsT" 作為查詢。在我們即將添加的 search_case_insensitive 函數(shù)中,查詢 "rUsT" 應(yīng)匹配包含大寫 R 的 "Rust:" 行,并匹配 "Trust me." 行,即使這兩行的大小寫與查詢不同。這是我們的失敗測試,它將無法編譯,因?yàn)槲覀冞€沒有定義 search_case_insensitive 函數(shù)。請隨意添加一個(gè)始終返回空向量的骨架實(shí)現(xiàn),類似于清單 12-16 中對 search 函數(shù)所做的實(shí)現(xiàn),以查看測試的編譯和失敗情況。
12.5.2 實(shí)現(xiàn)search_case_insensitive 功能
????????search_case_insensitive 函數(shù)(如清單 12-21 所示)與 search 函數(shù)幾乎相同。唯一不同的是,我們將小寫 query 和每個(gè) line ,因此無論輸入?yún)?shù)的大小寫是什么,當(dāng)我們檢查該行是否包含查詢時(shí),它們的大小寫都是一樣的。
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
(清單 12-21:定義 search_case_insensitive 函數(shù),以便在比較查詢和行之前小寫它們)
????????首先,我們將 query 字符串小寫,并將其存儲在同名的陰影變量中。在查詢時(shí)調(diào)用 to_lowercase 是必要的,這樣無論用戶的查詢是 "rust" 、 "RUST" 、 "Rust" 還是 "rUsT" ,我們都會將查詢當(dāng)作 "rust" 處理,而不區(qū)分大小寫。雖然 to_lowercase 可以處理基本的 Unicode,但并不是 100% 準(zhǔn)確。如果我們編寫的是一個(gè)真正的應(yīng)用程序,我們會在這里做更多的工作,但這一部分是關(guān)于環(huán)境變量的,而不是 Unicode,所以我們在這里就不多說了。
????????請注意, query 現(xiàn)在是 String ,而不是字符串片段,因?yàn)檎{(diào)用 to_lowercase 會創(chuàng)建新數(shù)據(jù),而不是引用現(xiàn)有數(shù)據(jù)。舉例來說,如果查詢結(jié)果是 "rUsT" :該字符串片段不包含小寫的 u 或 t 供我們使用,因此我們必須分配一個(gè)新的 String ,其中包含 "rust" ?,F(xiàn)在,當(dāng)我們將 query 作為參數(shù)傳遞給 contains 方法時(shí),我們需要添加一個(gè) "ampersand",因?yàn)?contains 的簽名被定義為接收一個(gè)字符串片段。
????????接下來,我們在每個(gè) line 上添加對 to_lowercase 的調(diào)用,以小寫所有字符。既然我們已經(jīng)將 line 和 query 轉(zhuǎn)換為小寫,那么無論查詢的大小寫是什么,我們都能找到匹配項(xiàng)。
????????讓我們看看這個(gè)實(shí)現(xiàn)是否通過了測試:
cargo.exe test
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src\main.rs (target\debug\deps\minigrep-9cd602df4200fb7d.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
????????好極了通過了?,F(xiàn)在,讓我們從 run 函數(shù)中調(diào)用新的 search_case_insensitive 函數(shù)。首先,我們將在 Config 結(jié)構(gòu)中添加一個(gè)配置選項(xiàng),以便在區(qū)分大小寫搜索和不區(qū)分大小寫搜索之間切換。添加這個(gè)字段會導(dǎo)致編譯器錯(cuò)誤,因?yàn)槲覀冞€沒有在任何地方初始化這個(gè)字段:
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
????????我們添加了保存布爾值的 ignore_case 字段。接下來,我們需要 run 函數(shù)來檢查 ignore_case 字段的值,并以此決定是調(diào)用 search 函數(shù)還是 search_case_insensitive 函數(shù),如清單 12-22 所示。這仍然無法編譯。
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
(清單 12-22:根據(jù) search 或 search_case_insensitive 中的值調(diào)用 config.ignore_case)
????????最后,我們需要檢查環(huán)境變量。處理環(huán)境變量的函數(shù)在標(biāo)準(zhǔn)庫的 env 模塊中,因此我們在 src/lib.rs 的頂部將該模塊引入作用域。然后,我們將使用 env 模塊中的 var 函數(shù)來檢查是否已為名為 IGNORE_CASE 的環(huán)境變量設(shè)置了值,如清單 12-23 所示。
use std::env;
// --snip--
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
(清單 12-23:檢查名為 IGNORE_CASE)
????????在此,我們創(chuàng)建了一個(gè)新變量 ignore_case 。為設(shè)置其值,我們調(diào)用 env::var 函數(shù),并將 IGNORE_CASE 環(huán)境變量的名稱傳給它。如果環(huán)境變量被設(shè)置為任何值, env::var 函數(shù)將返回一個(gè) Result ,它將是成功的 Ok 變體,其中包含環(huán)境變量的值。如果環(huán)境變量未設(shè)置,它將返回 Err 變體。
????????我們在 Result 上使用 is_ok 方法來檢查環(huán)境變量是否已設(shè)置,這意味著程序應(yīng)進(jìn)行大小寫不敏感搜索。如果 IGNORE_CASE 環(huán)境變量未設(shè)置為任何內(nèi)容, is_ok 將返回 false,程序?qū)?zhí)行大小寫敏感搜索。我們并不關(guān)心環(huán)境變量的值,只關(guān)心它是否被設(shè)置,所以我們要檢查 is_ok ,而不是使用 unwrap 、 expect 或我們在 Result 上見過的其他方法。
????????我們將 ignore_case 變量中的值傳遞給 Config 實(shí)例,這樣 run 函數(shù)就能讀取該值,并決定是調(diào)用 search_case_insensitive 還是 search ,如清單 12-22 所示。
????????讓我們試一試!首先,我們在不設(shè)置環(huán)境變量的情況下運(yùn)行程序,并使用查詢 to ,該查詢應(yīng)匹配任何包含小寫單詞 "to "的行:
cargo.exe run -- to .\poem.txt
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target\debug\minigrep.exe to .\poem.txt`
Searching for to
In file .\poem.txt
Are you nobody, too?
How dreary to be somebody!
????????看起來這仍然有效!現(xiàn)在,讓我們運(yùn)行程序,將 IGNORE_CASE 設(shè)置為 1 ,但查詢內(nèi)容相同 to 。
$ IGNORE_CASE=1 cargo run -- to poem.txt
????????如果使用的是 PowerShell,則需要設(shè)置環(huán)境變量,并將程序作為單獨(dú)的命令運(yùn)行:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
????????這將使 IGNORE_CASE 在 shell 會話的剩余時(shí)間內(nèi)持續(xù)存在??梢允褂?span style="color:#fe2c24;">Remove-Item命令 取消設(shè)置:
PS> Remove-Item Env:IGNORE_CASE
????????我們應(yīng)該得到包含大寫字母 "to "的行:
$Env:IGNORE_CASE=1;cargo run -- to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe to poem.txt`
Searching for to
In file poem.txt
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
????????很好,我們還得到了包含 "To "的行!現(xiàn)在,我們的 minigrep 程序可以在環(huán)境變量的控制下進(jìn)行不區(qū)分大小寫的搜索了。現(xiàn)在你知道如何管理使用命令行參數(shù)或環(huán)境變量設(shè)置的選項(xiàng)了吧。
????????有些程序允許在同一配置中使用參數(shù)和環(huán)境變量。在這種情況下,程序會決定其中一個(gè)優(yōu)先。如果你想自己再做一次練習(xí),可以嘗試通過命令行參數(shù)或環(huán)境變量來控制大小寫敏感性。如果程序運(yùn)行時(shí)一個(gè)設(shè)置為區(qū)分大小寫,一個(gè)設(shè)置為忽略大小寫,請決定是命令行參數(shù)優(yōu)先還是環(huán)境變量優(yōu)先。
????????std::env 模塊包含更多處理環(huán)境變量的有用功能:請查看其文檔了解可用功能。
12.6 將錯(cuò)誤信息寫入stderr而不是stdout
????????目前,我們使用 println! 宏將所有輸出寫入終端。在大多數(shù)終端中,有兩種輸出:用于一般信息的標(biāo)準(zhǔn)輸出( stdout )和用于錯(cuò)誤信息的標(biāo)準(zhǔn)錯(cuò)誤( stderr )。這種區(qū)別使用戶可以選擇將程序的成功輸出導(dǎo)出到文件,但仍將錯(cuò)誤信息打印到屏幕上(stderr)。
????????println! 宏只能打印到標(biāo)準(zhǔn)輸出,因此我們必須用其他方法打印到標(biāo)準(zhǔn)錯(cuò)誤。
12.6.1 檢查錯(cuò)誤書寫位置
????????首先,讓我們觀察一下 minigrep 打印的內(nèi)容目前是如何寫入標(biāo)準(zhǔn)輸出的,包括我們想寫入標(biāo)準(zhǔn)錯(cuò)誤的任何錯(cuò)誤信息。為此,我們將重定向標(biāo)準(zhǔn)輸出流到一個(gè)文件,同時(shí)故意造成錯(cuò)誤。我們不會重定向標(biāo)準(zhǔn)錯(cuò)誤流,因此發(fā)送到標(biāo)準(zhǔn)錯(cuò)誤的任何內(nèi)容都將繼續(xù)顯示在屏幕上。
????????命令行程序應(yīng)將錯(cuò)誤信息發(fā)送到標(biāo)準(zhǔn)錯(cuò)誤流,因此即使我們將標(biāo)準(zhǔn)輸出流重定向到文件,我們?nèi)阅茉谄聊簧峡吹藉e(cuò)誤信息。我們的程序目前并不乖巧:我們將看到它將錯(cuò)誤信息輸出保存到文件中!
????????為了演示這一行為,我們將運(yùn)行程序 > ,并輸入我們希望重定向標(biāo)準(zhǔn)輸出流的文件路徑 output.txt。我們將不傳遞任何參數(shù),因?yàn)檫@會導(dǎo)致出錯(cuò):
$ cargo run > output.txt
????????> 語法告訴 shell 將標(biāo)準(zhǔn)輸出的內(nèi)容寫入 output.txt,而不是屏幕。我們沒有看到預(yù)期的錯(cuò)誤信息被打印到屏幕上,這就意味著它最終一定被寫入了文件中。這就是 output.txt 文件的內(nèi)容:
Problem parsing arguments: not enough arguments
????????沒錯(cuò),我們的錯(cuò)誤信息被打印到了標(biāo)準(zhǔn)輸出中。將類似的錯(cuò)誤信息打印到標(biāo)準(zhǔn)錯(cuò)誤中會更有用,這樣只有成功運(yùn)行的數(shù)據(jù)才會被保存到文件中。我們將改變這一點(diǎn)。
12.6.2 將錯(cuò)誤打印到stderr上
????????我們將使用清單 12-24 中的代碼來更改錯(cuò)誤信息的打印方式。由于本章前面的重構(gòu),所有打印錯(cuò)誤信息的代碼都在一個(gè)函數(shù) main 中。標(biāo)準(zhǔn)庫提供了可打印到標(biāo)準(zhǔn)錯(cuò)誤流的 eprintln! 宏,因此我們將調(diào)用 println! 來打印錯(cuò)誤信息的兩個(gè)地方改為 eprintln! 。
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
(清單 12-24:將錯(cuò)誤信息寫入標(biāo)準(zhǔn)錯(cuò)誤而不是標(biāo)準(zhǔn)輸出,使用 eprintln!)
????????現(xiàn)在,讓我們以同樣的方式再次運(yùn)行程序,不使用任何參數(shù),并使用 > 重定向標(biāo)準(zhǔn)輸出:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
????????現(xiàn)在我們在屏幕上看到了錯(cuò)誤,而 output.txt 中什么也沒有,這正是我們所期望的命令行程序的行為。
????????讓我們再次運(yùn)行該程序,使用不會導(dǎo)致錯(cuò)誤但仍會將標(biāo)準(zhǔn)輸出重定向到文件的參數(shù),就像這樣:
cargo run -- to poem.txt > output.txt
Compiling minigrep v0.1.0 (E:\rustProj\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.39s
Running `target\debug\minigrep.exe to poem.txt`
????????我們在終端上看不到任何輸出,而 output.txt 將包含我們的結(jié)果:
Searching for to
In file poem.txt
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
????????這表明我們現(xiàn)在使用標(biāo)準(zhǔn)輸出進(jìn)行成功輸出,并酌情使用標(biāo)準(zhǔn)錯(cuò)誤進(jìn)行錯(cuò)誤輸出。
????????本章回顧了到目前為止所學(xué)到的一些主要概念,并介紹了如何在 Rust 中執(zhí)行常見的 I/O 操作。通過使用命令行參數(shù)、文件、環(huán)境變量和用于打印錯(cuò)誤的 eprintln! 宏,你已經(jīng)為編寫命令行應(yīng)用程序做好了準(zhǔn)備。結(jié)合前幾章中的概念,你的代碼將組織得井井有條,在適當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)中有效地存儲數(shù)據(jù),很好地處理錯(cuò)誤,并經(jīng)過良好的測試。文章來源:http://www.zghlxwxcb.cn/news/detail-829514.html
下一篇: 13-功能語言特征:迭代器和閉包文章來源地址http://www.zghlxwxcb.cn/news/detail-829514.html
到了這里,關(guān)于12-輸入/輸出項(xiàng)目構(gòu)建命令行程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!