本章既是一個(gè)目前所學(xué)的很多技能的概括,也是一個(gè)更多標(biāo)準(zhǔn)庫(kù)功能的探索。我們將構(gòu)建一個(gè)與文件和命令行輸入/輸出交互的命令行工具來(lái)練習(xí)現(xiàn)在一些你已經(jīng)掌握的 Rust 技能。
Rust 的運(yùn)行速度、安全性、單二進(jìn)制文件輸出和跨平臺(tái)支持使其成為創(chuàng)建命令行程序的絕佳選擇,所以我們的項(xiàng)目將創(chuàng)建一個(gè)我們自己版本的經(jīng)典命令行工具:grep
。grep 是 “Globally search a?Regular?Expression and?Print.” 的首字母縮寫(xiě)。grep
?最簡(jiǎn)單的使用場(chǎng)景是在特定文件中搜索指定字符串。為此,grep
?獲取一個(gè)文件名和一個(gè)字符串作為參數(shù),接著讀取文件并找到其中包含字符串參數(shù)的行,然后打印出這些行。
在這個(gè)過(guò)程中,我們會(huì)展示如何讓我們的命令行工具利用很多命令行工具中用到的終端功能。讀取環(huán)境變量來(lái)使得用戶可以配置工具的行為。打印到標(biāo)準(zhǔn)錯(cuò)誤控制流(stderr
) 而不是標(biāo)準(zhǔn)輸出(stdout
),例如這樣用戶可以選擇將成功輸出重定向到文件中的同時(shí)仍然在屏幕上顯示錯(cuò)誤信息。
12.1?接受命令參數(shù)行
一如既往使用cargo new 新建一個(gè)項(xiàng)目,我們稱之為minigrep 以便與可能已經(jīng)安裝在系統(tǒng)上的grep 工具相區(qū)別。
第一個(gè)任務(wù)是讓?minigrep
?能夠接受兩個(gè)命令行參數(shù):文件名和要搜索的字符串。也就是說(shuō)我們希望能夠使用?cargo run
、要搜索的字符串和被搜索的文件的路徑來(lái)運(yùn)行程序,像這樣:
cargo run searchstring example-filename.txt
讀取參數(shù)值
為了確保?minigrep
?能夠獲取傳遞給它的命令行參數(shù)的值,我們需要一個(gè) Rust 標(biāo)準(zhǔn)庫(kù)提供的函數(shù),也就是?std::env::args
。這個(gè)函數(shù)返回一個(gè)傳遞給程序的命令行參數(shù)的?迭代器(iterator)。但是現(xiàn)在只需理解迭代器的兩個(gè)細(xì)節(jié):迭代器生成一系列的值,可以在迭代器上調(diào)用?collect
?方法將其轉(zhuǎn)換為一個(gè)集合,比如包含所有迭代器產(chǎn)生元素的 vector。
use std::env;
fn main() {
let args : Vec<String> = env::args().collect();
println!("{:?}", args);
}
首先使用?use
?語(yǔ)句來(lái)將?std::env
?模塊引入作用域以便可以使用它的?args
?函數(shù)。注意?std::env::args
?函數(shù)被嵌套進(jìn)了兩層模塊中。
注意?
std::env::args
?在其任何參數(shù)包含無(wú)效 Unicode 字符時(shí)會(huì) panic。如果你需要接受包含無(wú)效 Unicode 字符的參數(shù),使用?std::env::args_os
?代替。這個(gè)函數(shù)返回?OsString
?值而不是?String
?值。這里出于簡(jiǎn)單考慮使用了?std::env::args
,因?yàn)?OsString
?值每個(gè)平臺(tái)都不一樣而且比?String
?值處理起來(lái)更為復(fù)雜。
在?main
?函數(shù)的第一行,我們調(diào)用了?env::args
,并立即使用?collect
?來(lái)創(chuàng)建了一個(gè)包含迭代器所有值的 vector。collect
?可以被用來(lái)創(chuàng)建很多類型的集合,所以這里顯式注明?args
?的類型來(lái)指定我們需要一個(gè)字符串 vector。雖然在 Rust 中我們很少會(huì)需要注明類型,然而?collect
?是一個(gè)經(jīng)常需要注明類型的函數(shù),因?yàn)?Rust 不能推斷出你想要什么類型的集合。
最后,我們使用調(diào)試格式?:?
?打印出 vector。讓我們嘗試分別用兩種方式(不包含參數(shù)和包含參數(shù))運(yùn)行代碼:
注意 vector 的第一個(gè)值是?"target/debug/minigrep"
,它是我們二進(jìn)制文件的名稱。這與 C 中的參數(shù)列表的行為相匹配,讓程序使用在執(zhí)行時(shí)調(diào)用它們的名稱。如果要在消息中打印它或者根據(jù)用于調(diào)用程序的命令行別名更改程序的行為,通??梢苑奖愕卦L問(wèn)程序名稱,不過(guò)考慮到本章的目的,我們將忽略它并只保存所需的兩個(gè)參數(shù)。
將參數(shù)值保存進(jìn)變量
打印出參數(shù) vector 中的值展示了程序可以訪問(wèn)指定為命令行參數(shù)的值?,F(xiàn)在需要將這兩個(gè)參數(shù)的值保存進(jìn)變量這樣就可以在程序的余下部分使用這些值了。
use std::env;
fn main() {
let args : Vec<String> = env::args().collect();
let query = &args[1]; // 索引0是文件名,所以從1開(kāi)始?
let file_name = &args[2];
println!("Searching for {}", query);
println!("In file {}", file_name);
}
結(jié)果
正如之前打印出 vector 時(shí)所所看到的,程序的名稱占據(jù)了 vector 的第一個(gè)值?args[0]
,所以我們從索引?1
?開(kāi)始。minigrep
?獲取的第一個(gè)參數(shù)是需要搜索的字符串,所以將其將第一個(gè)參數(shù)的引用存放在變量?query
?中。第二個(gè)參數(shù)將是文件名,所以將第二個(gè)參數(shù)的引用放入變量?filename
?中。
12.2?讀取文件
現(xiàn)在我們要增加讀取由?filename
?命令行參數(shù)指定的文件的功能。首先,需要一個(gè)用來(lái)測(cè)試的示例文件:用來(lái)確保?minigrep
?正常工作的最好的文件是擁有多行少量文本且有一些重復(fù)單詞的文件。
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!
創(chuàng)建完這個(gè)文件之后,修改代碼:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("Searching for {}", query);
println!("In file {}", filename);
println!("In file {}", filename);
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
結(jié)果
?首先,我們?cè)黾恿艘粋€(gè)?use
?語(yǔ)句來(lái)引入標(biāo)準(zhǔn)庫(kù)中的相關(guān)部分:我們需要?std::fs
?來(lái)處理文件。
在?main
?中新增了一行語(yǔ)句:fs::read_to_string
?接受?filename
,打開(kāi)文件,接著返回包含其內(nèi)容的?Result<String>
。
在這些代碼之后,我們?cè)俅卧黾恿伺R時(shí)的?println!
?打印出讀取文件之后?contents
?的值,這樣就可以檢查目前為止的程序能否工作。
12.3?重構(gòu)改進(jìn)模塊性和錯(cuò)誤處理
為了改善我們的程序這里有四個(gè)問(wèn)題需要修復(fù),而且他們都與程序的組織方式和如何處理潛在錯(cuò)誤有關(guān)。
第一,main
?現(xiàn)在進(jìn)行了兩個(gè)任務(wù):它解析了參數(shù)并打開(kāi)了文件。對(duì)于一個(gè)這樣的小函數(shù),這并不是一個(gè)大問(wèn)題。然而如果?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è)問(wèn)題:search
?和?filename
?是程序中的配置變量,而像?contents
?則用來(lái)執(zhí)行程序邏輯。隨著?main
?函數(shù)的增長(zhǎng),就需要引入更多的變量到作用域中,而當(dāng)作用域中有更多的變量時(shí),將更難以追蹤每個(gè)變量的目的。最好能將配置變量組織進(jìn)一個(gè)結(jié)構(gòu),這樣就能使他們的目的更明確了。
第三個(gè)問(wèn)題是如果打開(kāi)文件失敗我們使用?expect
?來(lái)打印出錯(cuò)誤信息,不過(guò)這個(gè)錯(cuò)誤信息只是說(shuō)?file not found
。除了缺少文件之外還有很多可以導(dǎo)致打開(kāi)文件失敗的方式:例如,文件可能存在,不過(guò)可能沒(méi)有打開(kāi)它的權(quán)限。如果我們現(xiàn)在就出于這種情況,打印出的?file not found
?錯(cuò)誤信息就給了用戶錯(cuò)誤的建議!
第四,我們不停地使用?expect
?來(lái)處理不同的錯(cuò)誤,如果用戶沒(méi)有指定足夠的參數(shù)來(lái)運(yùn)行程序,他們會(huì)從 Rust 得到?index out of bounds
?錯(cuò)誤,而這并不能明確地解釋問(wèn)題。如果所有的錯(cuò)誤處理都位于一處,這樣將來(lái)的維護(hù)者在需要修改錯(cuò)誤處理邏輯時(shí)就只需要考慮這一處代碼。將所有的錯(cuò)誤處理都放在一處也有助于確保我們打印的錯(cuò)誤信息對(duì)終端用戶來(lái)說(shuō)是有意義的。
二進(jìn)制項(xiàng)目的關(guān)注分離
這些過(guò)程有如下步驟:
- 將程序拆分成?main.rs?和?lib.rs?并將程序的邏輯放入?lib.rs?中。
- 當(dāng)命令行解析邏輯比較小時(shí),可以保留在?main.rs?中。
- 當(dāng)命令行解析開(kāi)始變得復(fù)雜時(shí),也同樣將其從?main.rs?提取到?lib.rs?中。
經(jīng)過(guò)這些過(guò)程之后保留在?main
?函數(shù)中的責(zé)任應(yīng)該被限制為:
- 使用參數(shù)值調(diào)用命令行解析邏輯
- 設(shè)置任何其他的配置
- 調(diào)用?lib.rs?中的?
run
?函數(shù) - 如果?
run
?返回錯(cuò)誤,則處理這個(gè)錯(cuò)誤
提取參數(shù)解析器
首先,我們將解析參數(shù)的功能提取到一個(gè)?main
?將會(huì)調(diào)用的函數(shù)中,為將命令行解析邏輯移動(dòng)到?src/lib.rs?中做準(zhǔn)備。示例中展示了新?main
?函數(shù)的開(kāi)頭,它調(diào)用了新函數(shù)?parse_config
。目前它仍將定義在?src/main.rs?中:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
我們?nèi)匀粚⒚钚袇?shù)收集進(jìn)一個(gè) vector,不過(guò)不同于在?main
?函數(shù)中將索引 1 的參數(shù)值賦值給變量?query
?和將索引 2 的值賦值給變量?filename
,我們將整個(gè) vector 傳遞給?parse_config
?函數(shù)。接著?parse_config
?函數(shù)將包含決定哪個(gè)參數(shù)該放入哪個(gè)變量的邏輯,并將這些值返回到?main
。仍然在?main
?中創(chuàng)建變量?query
?和?filename
,不過(guò)?main
?不再負(fù)責(zé)處理命令行參數(shù)與變量如何對(duì)應(yīng)。
組合配置值
現(xiàn)在函數(shù)返回一個(gè)元組,不過(guò)立刻又將元組拆成了獨(dú)立的部分。這是一個(gè)我們可能沒(méi)有進(jìn)行正確抽象的信號(hào)。
另一個(gè)表明還有改進(jìn)空間的跡象是?parse_config
?名稱的?config
?部分,它暗示了我們返回的兩個(gè)值是相關(guān)的并都是一個(gè)配置值的一部分。目前除了將這兩個(gè)值組合進(jìn)元組之外并沒(méi)有表達(dá)這個(gè)數(shù)據(jù)結(jié)構(gòu)的意義:我們可以將這兩個(gè)值放入一個(gè)結(jié)構(gòu)體并給每個(gè)字段一個(gè)有意義的名字。這會(huì)讓未來(lái)的維護(hù)者更容易理解不同的值如何相互關(guān)聯(liá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.filename);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
// 抽象成結(jié)構(gòu)體
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}
}
新定義的結(jié)構(gòu)體?Config
?中包含字段?query
?和?filename
。?parse_config
?的簽名表明它現(xiàn)在返回一個(gè)?Config
?值。在之前的?parse_config
?函數(shù)體中,我們返回了引用?args
?中?String
?值的字符串 slice,現(xiàn)在我們定義?Config
?來(lái)包含擁有所有權(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ù)的完整拷貝,不過(guò)會(huì)比儲(chǔ)存字符串?dāng)?shù)據(jù)的引用消耗更多的時(shí)間和內(nèi)存。不過(guò)拷貝數(shù)據(jù)使得代碼顯得更加直白因?yàn)闊o(wú)需管理引用的生命周期,所以在這種情況下?tīng)奚恍〔糠中阅軄?lái)?yè)Q取簡(jiǎn)潔性的取舍是值得的。
使用clone的權(quán)衡取舍
由于其運(yùn)行時(shí)消耗,許多 Rustacean 之間有一個(gè)趨勢(shì)是傾向于避免使用?clone
?來(lái)解決所有權(quán)問(wèn)題。在關(guān)于迭代器的第十三章中,我們將會(huì)學(xué)習(xí)如何更有效率的處理這種情況,不過(guò)現(xiàn)在,復(fù)制一些字符串來(lái)取得進(jìn)展是沒(méi)有問(wèn)題的,因?yàn)橹粫?huì)進(jìn)行一次這樣的拷貝,而且文件名和要搜索的字符串都比較短。在第一輪編寫(xiě)時(shí)擁有一個(gè)可以工作但有點(diǎn)低效的程序要比嘗試過(guò)度優(yōu)化代碼更好一些。隨著你對(duì) Rust 更加熟練,將能更輕松的直奔合適的方法,不過(guò)現(xiàn)在調(diào)用?clone
?是完全可以接受的。
創(chuàng)建一個(gè)Config 的構(gòu)造函數(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
?來(lái)創(chuàng)建一個(gè)該類型的實(shí)例那樣,將?parse_config
?變?yōu)橐粋€(gè)與?Config
?關(guān)聯(lián)的?new
?函數(shù)。
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.filename);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
// 抽象成結(jié)構(gòu)體
struct Config {
query: String,
filename: String,
}
// 構(gòu)造函數(shù)
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}
這里將?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)在我們開(kāi)始修復(fù)錯(cuò)誤處理?;貞浺幌轮疤岬竭^(guò)如果?args
?vector 包含少于 3 個(gè)項(xiàng)并嘗試訪問(wèn) vector 中索引?1
?或索引?2
?的值會(huì)造成程序 panic。嘗試不帶任何參數(shù)運(yùn)行程序;這將看起來(lái)像這樣:
改善錯(cuò)誤信息
在?new
?函數(shù)中增加了一個(gè)檢查在訪問(wèn)索引?1
?和?2
?之前檢查 slice 是否足夠長(zhǎng)。如果 slice 不夠長(zhǎng),我們使用一個(gè)更好的錯(cuò)誤信息 panic 而不是?index out of bounds
?信息:
// 構(gòu)造函數(shù)
impl Config {
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}
有了?new
?中這幾行額外的代碼,再次不帶任何參數(shù)運(yùn)行程序并看看現(xiàn)在錯(cuò)誤看起來(lái)像什么:
從new中返回Result而不是調(diào)用panic!
我們可以選擇返回一個(gè)?Result
?值,它在成功時(shí)會(huì)包含一個(gè)?Config
?的實(shí)例,而在錯(cuò)誤時(shí)會(huì)描述問(wèn)題。當(dāng)?Config::new
?與?main
?交流時(shí),可以使用?Result
?類型來(lái)表明這里存在問(wèn)題。接著修改?main
?將?Err
?成員轉(zhuǎn)換為對(duì)用戶更友好的錯(cuò)誤,而不是?panic!
?調(diào)用產(chǎn)生的關(guān)于?thread 'main'
?和?RUST_BACKTRACE
?的文本。
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
// 這詭異的寫(xiě)法
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
// 抽象成結(jié)構(gòu)體
struct Config {
query: String,
filename: String,
}
// 構(gòu)造函數(shù),這里返回值有變化
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})
}
}
現(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)沒(méi)有足夠參數(shù)時(shí)不再調(diào)用?panic!
,而是返回?Err
?值。同時(shí)我們將?Config
?返回值包裝進(jìn)?Ok
?成員中。這些修改使得函數(shù)符合其新的類型簽名。
通過(guò)讓?Config::new
?返回一個(gè)?Err
?值,這就允許?main
?函數(shù)處理?new
?函數(shù)返回的?Result
?值并在出現(xiàn)錯(cuò)誤的情況更明確的結(jié)束進(jìn)程。
在上面的示例中,使用了一個(gè)之前沒(méi)有涉及到的方法: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ù)。
新增了一個(gè)?use
?行來(lái)從標(biāo)準(zhǔn)庫(kù)中導(dǎo)入?process
。在錯(cuò)誤的情況閉包中將被運(yùn)行的代碼只有兩行:我們打印出了?err
?值,接著調(diào)用了?std::process::exit
。process::exit
?會(huì)立即停止程序并將傳遞給它的數(shù)字作為退出狀態(tài)碼。
寫(xiě)法太詭異了
從main提取邏輯
目前我們只進(jìn)行小的增量式的提取函數(shù)的改進(jìn)。我們?nèi)詫⒃?src/main.rs?中定義這個(gè)函數(shù):
fn main() {
let args: Vec<String> = env::args().collect();
// 這詭異的寫(xiě)法
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
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);
}
// ...
從run函數(shù)中返回錯(cuò)誤
進(jìn)一步以一種對(duì)用戶友好的方式統(tǒng)一?main
?中的錯(cuò)誤處理。
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(())
}
這里我們做出了三個(gè)明顯的修改。首先,將?run
?函數(shù)的返回類型變?yōu)?Result<(), Box<dyn Error>>
。之前這個(gè)函數(shù)返回 unit 類型?()
,現(xiàn)在它仍然保持作為?Ok
?時(shí)的返回值。
第二個(gè)改變是去掉了?expect
?調(diào)用并替換為第九章講到的??
。不同于遇到錯(cuò)誤就?panic!
,?
?會(huì)從函數(shù)中返回錯(cuò)誤值并讓調(diào)用者來(lái)處理它。
第三個(gè)修改是現(xiàn)在成功時(shí)這個(gè)函數(shù)會(huì)返回一個(gè)?Ok
?值。因?yàn)?run
?函數(shù)簽名中聲明成功類型返回值是?()
,這意味著需要將 unit 類型值包裝進(jìn)?Ok
?值中。Ok(())
?一開(kāi)始看起來(lái)有點(diǎn)奇怪,不過(guò)這樣使用?()
?是表明我們調(diào)用?run
?只是為了它的副作用的慣用方式;它并沒(méi)有返回什么有意義的值。
Rust 提示我們的代碼忽略了?Result
?值,它可能表明這里存在一個(gè)錯(cuò)誤。雖然我們沒(méi)有檢查這里是否有一個(gè)錯(cuò)誤,而編譯器提醒我們這里應(yīng)該有一些錯(cuò)誤處理代碼!現(xiàn)在就讓我們修正這個(gè)問(wèn)題。
處理main 中run返回的錯(cuò)誤
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
?來(lái)檢查?run
?是否返回一個(gè)?Err
?值,不同于?unwrap_or_else
,并在出錯(cuò)時(shí)調(diào)用?process::exit(1)
。run
?并不返回像?Config::new
?返回的?Config
?實(shí)例那樣需要?unwrap
?的值。因?yàn)?run
?在成功時(shí)返回?()
,而我們只關(guān)心檢測(cè)錯(cuò)誤,所以并不需要?unwrap_or_else
?來(lái)返回未封裝的值,因?yàn)樗粫?huì)是?()
。
將代碼拆分到庫(kù)crate
讓我們將所有不是?main
?函數(shù)的代碼從?src/main.rs?移動(dòng)到新文件?src/lib.rs?中:
-
run
?函數(shù)定義 - 相關(guān)的?
use
?語(yǔ)句 -
Config
?的定義 -
Config::new
?函數(shù)定義
lib.rs
use std::fs;
use std::error::Error;
// 結(jié)構(gòu)體
pub struct Config {
pub query: String,
pub filename: String,
}
// 構(gòu)造函數(shù),這里返回值有變化
impl Config {
pub 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})
}
}
// run函數(shù)
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
// 這詭異的寫(xiě)法
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
// 注意run的引用方式
if let Err(e) = minigrep::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
?這里使用了公有的?pub
?關(guān)鍵字:在?Config
、其字段和其?new
?方法,以及?run
?函數(shù)上?,F(xiàn)在我們有了一個(gè)擁有可以測(cè)試的公有 API 的庫(kù) crate 了。
為了將庫(kù) crate 引入二進(jìn)制 crate,我們使用了?use minigrep
。接著?use minigrep::Config
?將?Config
?類型引入作用域,并使用 crate 名稱作為?run
?函數(shù)的前綴。通過(guò)這些重構(gòu),所有功能應(yīng)該能夠聯(lián)系在一起并運(yùn)行了。運(yùn)行?cargo run
?來(lái)確保一切都正確的銜接在一起。
13.4?采用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)完善庫(kù)的功能
在這一部分,我們將遵循測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test Driven Development, TDD)的模式來(lái)逐步增加?minigrep
?的搜索邏輯。這是一個(gè)軟件開(kāi)發(fā)技術(shù),它遵循如下步驟:
- 編寫(xiě)一個(gè)會(huì)失敗的測(cè)試,并運(yùn)行它以確保其因?yàn)槟闫谕脑蚴 ?/li>
- 編寫(xiě)或修改剛好足夠的代碼來(lái)使得新的測(cè)試通過(guò)。
- 重構(gòu)剛剛增加或修改的代碼,并確保測(cè)試仍然能通過(guò)。
- 從步驟 1 開(kāi)始重復(fù)!
這只是眾多編寫(xiě)軟件的方法之一,不過(guò) TDD 有助于驅(qū)動(dòng)代碼的設(shè)計(jì)。在編寫(xiě)能使測(cè)試通過(guò)的代碼之前編寫(xiě)測(cè)試有助于在開(kāi)發(fā)過(guò)程中保持高測(cè)試覆蓋率。
編寫(xiě)失敗測(cè)試
去掉?src/lib.rs?和?src/main.rs?中用于檢查程序行為的?println!
?語(yǔ)句,因?yàn)椴辉僬嬲枰麄兞?。加入測(cè)試。測(cè)試函數(shù)指定了?search
?函數(shù)期望擁有的行為:它會(huì)獲取一個(gè)需要查詢的字符串和用來(lái)查詢的文本,并只會(huì)返回包含請(qǐng)求的文本行。
文件名: src/lib.rs
#[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)
);
}
}
這里選擇使用?"duct"
?作為這個(gè)測(cè)試中需要搜索的字符串。用來(lái)搜索的文本有三行,其中只有一行包含?"duct"
。我們斷言?search
?函數(shù)的返回值只包含期望的那一行。
我們還不能運(yùn)行這個(gè)測(cè)試并看到它失敗,因?yàn)樗踔炼歼€不能編譯:search
?函數(shù)還不存在呢!我們將增加足夠的代碼來(lái)使其能夠編譯:一個(gè)總是會(huì)返回空 vector 的?search
?函數(shù)定義
文件名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
注意需要在?search
?的簽名中定義一個(gè)顯式生命周期?'a
?并用于?contents
?參數(shù)和返回值?;貞浺幌碌谑轮兄v到生命周期參數(shù)指定哪個(gè)參數(shù)的生命周期與返回值的生命周期相關(guān)聯(lián)。在這個(gè)例子中,我們表明返回的 vector 中應(yīng)該包含引用參數(shù)?contents
(而不是參數(shù)query
) slice 的字符串 slice。
換句話說(shuō),我們告訴 Rust 函數(shù)?search
?返回的數(shù)據(jù)將與?search
?函數(shù)中的參數(shù)?contents
?的數(shù)據(jù)存在的一樣久。這是非常重要的!為了使這個(gè)引用有效那么?被?slice 引用的數(shù)據(jù)也需要保持有效;如果編譯器認(rèn)為我們是在創(chuàng)建?query
?而不是?contents
?的字符串 slice,那么安全檢查將是不正確的。
編寫(xiě)使測(cè)試通過(guò)的代碼
目前測(cè)試之所以會(huì)失敗是因?yàn)槲覀兛偸欠祷匾粋€(gè)空的 vector。為了修復(fù)并實(shí)現(xiàn)?search
,我們的程序需要遵循如下步驟:
- 遍歷內(nèi)容的每一行文本。
- 查看這一行是否包含要搜索的字符串。
- 如果有,將這一行加入列表返回值中。
- 如果沒(méi)有,什么也不做。
- 返回匹配到的結(jié)果列表
讓我們一步一步的來(lái),從遍歷每行開(kāi)始。
使用lines方法遍歷每—行
Rust有一個(gè)有助于一行一行遍歷字符串的方法,出于方便它被命名為lines,它如示例這樣工作。注意這還不能編譯:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something
}
}
lines
?方法返回一個(gè)迭代器。
用查詢寧符串搜索每一行
接下來(lái)將會(huì)增加檢查當(dāng)前行是否包含查詢字符串的功能。幸運(yùn)的是,字符串類型為此也有一個(gè)叫做
contains的實(shí)用方法!如示例所示在search函數(shù)中加入contains方法調(diào)用。注意這仍然不能編譯:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
//
}
}
}
存儲(chǔ)匹配的行
我們還需要一個(gè)方法來(lái)存儲(chǔ)包含查詢字符串的行。為此可以在?for
?循環(huán)之前創(chuàng)建一個(gè)可變的 vector 并調(diào)用?push
?方法在 vector 中存放一個(gè)?line
。在?for
?循環(huán)之后,返回這個(gè) vector
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
}
現(xiàn)在?search
?函數(shù)應(yīng)該返回只包含?query
?的那些行,而測(cè)試應(yīng)該會(huì)通過(guò)。讓我們運(yùn)行測(cè)試:
到此為止,我們可以考慮一下重構(gòu)?search
?的實(shí)現(xiàn)并時(shí)刻保持測(cè)試通過(guò)來(lái)保持其功能不變的機(jī)會(huì)了。search
?函數(shù)中的代碼并不壞,不過(guò)并沒(méi)有利用迭代器的一些實(shí)用功能。
在run函數(shù)中使用search函數(shù)
現(xiàn)在?search
?函數(shù)是可以工作并測(cè)試通過(guò)了的,我們需要實(shí)際在?run
?函數(shù)中調(diào)用?search
。需要將?config.query
?值和?run
?從文件中讀取的?contents
?傳遞給?search
?函數(shù)。接著?run
?會(huì)打印出?search
?返回的每一行:
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
?這里仍然使用了?for
?循環(huán)獲取了?search
?返回的每一行并打印出來(lái)。
OK了
13.5?處理環(huán)境變量
將增加一個(gè)額外的功能來(lái)改進(jìn)?minigrep
:用戶可以通過(guò)設(shè)置環(huán)境變量來(lái)設(shè)置搜索是否是大小寫(xiě)敏感的 。當(dāng)然,我們也可以將其設(shè)計(jì)為一個(gè)命令行參數(shù)并要求用戶每次需要時(shí)都加上它,不過(guò)在這里我們將使用環(huán)境變量。這允許用戶設(shè)置環(huán)境變量一次之后在整個(gè)終端會(huì)話中所有的搜索都將是大小寫(xiě)不敏感的。
編寫(xiě)一個(gè)大小寫(xiě)不敏感search函數(shù)的失敗測(cè)試
希望增加一個(gè)新函數(shù)?search_case_insensitive
,并將會(huì)在設(shè)置了環(huán)境變量時(shí)調(diào)用它。這里將繼續(xù)遵循 TDD 過(guò)程,其第一步是再次編寫(xiě)一個(gè)失敗測(cè)試。我們將為新的大小寫(xiě)不敏感搜索函數(shù)新增一個(gè)測(cè)試函數(shù),并將老的測(cè)試函數(shù)從?one_result
?改名為?case_sensitive
?來(lái)更清楚的表明這兩個(gè)測(cè)試的區(qū)別
文件名: src/lib.rs
#[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)
);
}
}
注意我們也改變了老測(cè)試中?contents
?的值。還新增了一個(gè)含有文本?"Duct tape."
?的行,它有一個(gè)大寫(xiě)的 D,這在大小寫(xiě)敏感搜索時(shí)不應(yīng)該匹配 "duct"。我們修改這個(gè)測(cè)試以確保不會(huì)意外破壞已經(jīng)實(shí)現(xiàn)的大小寫(xiě)敏感搜索功能;這個(gè)測(cè)試現(xiàn)在應(yīng)該能通過(guò)并在處理大小寫(xiě)不敏感搜索時(shí)應(yīng)該能一直通過(guò)。
大小寫(xiě)?不敏感?搜索的新測(cè)試使用?"rUsT"
?作為其查詢字符串。在我們將要增加的?search_case_insensitive
?函數(shù)中,"rUsT"
?查詢應(yīng)該包含帶有一個(gè)大寫(xiě) R 的?"Rust:"
?還有?"Trust me."
?這兩行,即便他們與查詢的大小寫(xiě)都不同。這個(gè)測(cè)試現(xiàn)在會(huì)編譯失敗因?yàn)檫€沒(méi)有定義?search_case_insensitive
?函數(shù)。
實(shí)現(xiàn)search_case_insensitive函數(shù)
search_case_insensitive
?函數(shù),將與?search
?函數(shù)基本相同。唯一的區(qū)別是它會(huì)將?query
?變量和每一?line
?都變?yōu)樾?xiě),這樣不管輸入?yún)?shù)是大寫(xiě)還是小寫(xiě),在檢查該行是否包含查詢字符串時(shí)都會(huì)是小寫(xiě)。
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
}
首先我們將?query
?字符串轉(zhuǎn)換為小寫(xiě),并將其覆蓋到同名的變量中。對(duì)查詢字符串調(diào)用?to_lowercase
?是必需的,這樣不管用戶的查詢是?"rust"
、"RUST"
、"Rust"
?或者?"rUsT"
,我們都將其當(dāng)作?"rust"
?處理并對(duì)大小寫(xiě)不敏感。
注意?query
?現(xiàn)在是一個(gè)?String
?而不是字符串 slice,因?yàn)檎{(diào)用?to_lowercase
?是在創(chuàng)建新數(shù)據(jù),而不是引用現(xiàn)有數(shù)據(jù)。如果查詢字符串是?"rUsT"
,這個(gè)字符串 slice 并不包含可供我們使用的小寫(xiě)的?u
?或?t
,所以必需分配一個(gè)包含?"rust"
?的新?String
?,F(xiàn)在當(dāng)我們將?query
?作為一個(gè)參數(shù)傳遞給?contains
?方法時(shí),需要增加一個(gè) & 因?yàn)?contains
?的簽名被定義為獲取一個(gè)字符串 slice。
接下來(lái)在檢查每個(gè)?line
?是否包含?search
?之前增加了一個(gè)?to_lowercase
?調(diào)用將他們都變?yōu)樾?xiě)?,F(xiàn)在我們將?line
?和?query
?都轉(zhuǎn)換成了小寫(xiě),這樣就可以不管查詢的大小寫(xiě)進(jìn)行匹配了。
好的!現(xiàn)在,讓我們?cè)?run
?函數(shù)中實(shí)際調(diào)用新?search_case_insensitive
?函數(shù)。首先,我們將在?Config
?結(jié)構(gòu)體中增加一個(gè)配置項(xiàng)來(lái)切換大小寫(xiě)敏感和大小寫(xiě)不敏感搜索。增加這些字段會(huì)導(dǎo)致編譯錯(cuò)誤,因?yàn)槲覀冞€沒(méi)有在任何地方初始化這些字段:
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
這里增加了?case_sensitive
?字符來(lái)存放一個(gè)布爾值。接著我們需要?run
?函數(shù)檢查?case_sensitive
?字段的值并使用它來(lái)決定是否調(diào)用?search
?函數(shù)或?search_case_insensitive
?函數(shù),如示例所示。注意這還不能編譯:
文件名: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
最后需要實(shí)際檢查環(huán)境變量。處理環(huán)境變量的函數(shù)位于標(biāo)準(zhǔn)庫(kù)的?env
?模塊中,所以我們需要在?src/lib.rs?的開(kāi)頭增加一個(gè)?use std::env;
?行將這個(gè)模塊引入作用域中。接著在?Config::new
?中使用?env
?模塊的?var
?方法來(lái)檢查一個(gè)叫做?CASE_INSENSITIVE
?的環(huán)境變量。文件名: src/lib.rs
use std::env;
// --snip--
impl Config {
pub 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();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
這里創(chuàng)建了一個(gè)新變量?case_sensitive
。為了設(shè)置它的值,需要調(diào)用?env::var
?函數(shù)并傳遞我們需要尋找的環(huán)境變量名稱,CASE_INSENSITIVE
。env::var
?返回一個(gè)?Result
,它在環(huán)境變量被設(shè)置時(shí)返回包含其值的?Ok
?成員,并在環(huán)境變量未被設(shè)置時(shí)返回?Err
?成員。
我們使用?Result
?的?is_err
?方法來(lái)檢查其是否是一個(gè) error(也就是環(huán)境變量未被設(shè)置的情況),這也就意味著我們?需要?進(jìn)行一個(gè)大小寫(xiě)敏感搜索。如果CASE_INSENSITIVE
?環(huán)境變量被設(shè)置為任何值,is_err
?會(huì)返回 false 并將進(jìn)行大小寫(xiě)不敏感搜索。我們并不關(guān)心環(huán)境變量所設(shè)置的?值,只關(guān)心它是否被設(shè)置了,所以檢查?is_err
?而不是?unwrap
、expect
?或任何我們已經(jīng)見(jiàn)過(guò)的?Result
?的方法。
?看起來(lái)程序仍然能夠工作!現(xiàn)在將?CASE_INSENSITIVE
?設(shè)置為?1
?并仍使用相同的查詢?to
。
如果你使用 PowerShell,則需要用兩個(gè)命令來(lái)設(shè)置環(huán)境變量并運(yùn)行程序:
$ $env:CASE_INSENSITIVE=1
$ cargo run to poem.txt
13.6?將錯(cuò)誤信息輸出到標(biāo)準(zhǔn)錯(cuò)誤而不是標(biāo)準(zhǔn)輸出
檢查錯(cuò)誤應(yīng)該寫(xiě)入何處
首先,讓我們觀察一下目前?minigrep
?打印的所有內(nèi)容是如何被寫(xiě)入標(biāo)準(zhǔn)輸出的,包括那些應(yīng)該被寫(xiě)入標(biāo)準(zhǔn)錯(cuò)誤的錯(cuò)誤信息??梢酝ㄟ^(guò)將標(biāo)準(zhǔn)輸出流重定向到一個(gè)文件同時(shí)有意產(chǎn)生一個(gè)錯(cuò)誤來(lái)做到這一點(diǎn)。我們沒(méi)有重定向標(biāo)準(zhǔn)錯(cuò)誤流,所以任何發(fā)送到標(biāo)準(zhǔn)錯(cuò)誤的內(nèi)容將會(huì)繼續(xù)顯示在屏幕上。
命令行程序被期望將錯(cuò)誤信息發(fā)送到標(biāo)準(zhǔn)錯(cuò)誤流,這樣即便選擇將標(biāo)準(zhǔn)輸出流重定向到文件中時(shí)仍然能看到錯(cuò)誤信息。目前我們的程序并不符合期望;相反我們將看到它將錯(cuò)誤信息輸出保存到了文件中。
我們通過(guò)?>
?和文件名?output.txt?來(lái)運(yùn)行程序,我們期望重定向標(biāo)準(zhǔn)輸出流到該文件中。在這里,我們沒(méi)有傳遞任何參數(shù),所以會(huì)產(chǎn)生一個(gè)錯(cuò)誤:
$ cargo run > output.txt
結(jié)果
將錯(cuò)誤打印到標(biāo)準(zhǔn)錯(cuò)誤
標(biāo)準(zhǔn)庫(kù)提供了?eprintln!
?宏來(lái)打印到標(biāo)準(zhǔn)錯(cuò)誤流,所以將兩個(gè)調(diào)用?println!
?打印錯(cuò)誤信息的位置替換為?eprintln!
:
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}
再次嘗試用同樣的方式運(yùn)行程序,不使用任何參數(shù)并通過(guò)?>
?重定向標(biāo)準(zhǔn)輸出:
現(xiàn)在我們看到了屏幕上的錯(cuò)誤信息,同時(shí)?output.txt?里什么也沒(méi)有,這正是命令行程序所期望的行為。
如果使用不會(huì)造成錯(cuò)誤的參數(shù)再次運(yùn)行程序,不過(guò)仍然將標(biāo)準(zhǔn)輸出重定向到一個(gè)文件,像這樣:
$ cargo run to poem.txt > output.txt
我們并不會(huì)在終端看到任何輸出,同時(shí)?output.txt
?將會(huì)包含其結(jié)果:
文件名: output.txt
Are you nobody, too?
How dreary to be somebody!
結(jié)果
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-655729.html
參考:一個(gè) I/O 項(xiàng)目:構(gòu)建命令行程序 - Rust 程序設(shè)計(jì)語(yǔ)言 簡(jiǎn)體中文版 (bootcss.com)文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-655729.html
到了這里,關(guān)于【Rust】Rust學(xué)習(xí) 第十二章一個(gè) I/O 項(xiàng)目:構(gòu)建一個(gè)命令行程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!