ch13-03-improving-our-io-project.md
commit cc958ca579816ea6ac7e9067d628b0423a1ed3e4
有了這些關(guān)于迭代器的新知識,我們可以使用迭代器來改進(jìn)第十二章中 I/O 項目的實現(xiàn)來使得代碼更簡潔明了。讓我們看看迭代器如何能夠改進(jìn) Config::new
函數(shù)和 search
函數(shù)的實現(xiàn)。
在示例 12-6 中,我們增加了一些代碼獲取一個 String
slice 并創(chuàng)建一個 Config
結(jié)構(gòu)體的實例,他們索引 slice 中的值并克隆這些值以便 Config
結(jié)構(gòu)體可以擁有這些值。在示例 13-24 中重現(xiàn)了第十二章結(jié)尾示例 12-23 中 Config::new
函數(shù)的實現(xiàn):
文件名: src/lib.rs
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,
})
}
}
示例 13-24:重現(xiàn)第十二章結(jié)尾的 Config::new
函數(shù)
那時我們說過不必?fù)?dān)心低效的 clone
調(diào)用了,因為將來可以對他們進(jìn)行改進(jìn)。好吧,就是現(xiàn)在!
起初這里需要 clone
的原因是參數(shù) args
中有一個 String
元素的 slice,而 new
函數(shù)并不擁有 args
。為了能夠返回 Config
實例的所有權(quán),我們需要克隆 Config
中字段 query
和 filename
的值,這樣 Config
實例就能擁有這些值。
在學(xué)習(xí)了迭代器之后,我們可以將 new
函數(shù)改為獲取一個有所有權(quán)的迭代器作為參數(shù)而不是借用 slice。我們將使用迭代器功能之前檢查 slice 長度和索引特定位置的代碼。這會明確 Config::new
的工作因為迭代器會負(fù)責(zé)訪問這些值。
一旦 Config::new
獲取了迭代器的所有權(quán)并不再使用借用的索引操作,就可以將迭代器中的 String
值移動到 Config
中,而不是調(diào)用 clone
分配新的空間。
打開 I/O 項目的 src/main.rs 文件,它看起來應(yīng)該像這樣:
文件名: src/main.rs
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);
});
// --snip--
}
修改第十二章結(jié)尾示例 12-24 中的 main
函數(shù)的開頭為示例 13-25 中的代碼。在更新 Config::new
之前這些代碼還不能編譯:
文件名: src/main.rs
fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
示例 13-25:將 env::args
的返回值傳遞給 Config::new
env::args
函數(shù)返回一個迭代器!不同于將迭代器的值收集到一個 vector 中接著傳遞一個 slice 給 Config::new
,現(xiàn)在我們直接將 env::args
返回的迭代器的所有權(quán)傳遞給 Config::new
。
接下來需要更新 Config::new
的定義。在 I/O 項目的 src/lib.rs 中,將 Config::new
的簽名改為如示例 13-26 所示。這仍然不能編譯因為我們還需更新函數(shù)體:
文件名: src/lib.rs
impl Config {
pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
// --snip--
示例 13-26:以迭代器作為參數(shù)更新 Config::new
的簽名
env::args
函數(shù)的標(biāo)準(zhǔn)庫文檔顯示,它返回的迭代器的類型為 std::env::Args
。我們已經(jīng)更新了 Config :: new
函數(shù)的簽名,因此參數(shù) args
的類型為 std::env::Args
而不是 &[String]
。因為我們擁有 args
的所有權(quán),并且將通過對其進(jìn)行迭代來改變 args
,所以我們可以將 mut
關(guān)鍵字添加到 args
參數(shù)的規(guī)范中以使其可變。
接下來,我們將修改 Config::new
的內(nèi)容。標(biāo)準(zhǔn)庫文檔還提到 std::env::Args
實現(xiàn)了 Iterator
trait,因此我們知道可以對其調(diào)用 next
方法!示例 13-27 更新了示例 12-23 中的代碼,以使用 next
方法:
文件名: src/lib.rs
impl Config {
pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}
示例 13-27:修改 Config::new
的函數(shù)體來使用迭代器方法
請記住 env::args
返回值的第一個值是程序的名稱。我們希望忽略它并獲取下一個值,所以首先調(diào)用 next
并不對返回值做任何操作。之后對希望放入 Config
中字段 query
調(diào)用 next
。如果 next
返回 Some
,使用 match
來提取其值。如果它返回 None
,則意味著沒有提供足夠的參數(shù)并通過 Err
值提早返回。對 filename
值進(jìn)行同樣的操作。
I/O 項目中其他可以利用迭代器的地方是 search
函數(shù),示例 13-28 中重現(xiàn)了第十二章結(jié)尾示例 12-19 中此函數(shù)的定義:
文件名: src/lib.rs
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
}
示例 13-28:示例 12-19 中 search
函數(shù)的定義
可以通過使用迭代器適配器方法來編寫更簡明的代碼。這也避免了一個可變的中間 results
vector 的使用。函數(shù)式編程風(fēng)格傾向于最小化可變狀態(tài)的數(shù)量來使代碼更簡潔。去掉可變狀態(tài)可能會使得將來進(jìn)行并行搜索的增強(qiáng)變得更容易,因為我們不必管理 results
vector 的并發(fā)訪問。示例 13-29 展示了該變化:
文件名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
示例 13-29:在 search
函數(shù)實現(xiàn)中使用迭代器適配器
回憶 search
函數(shù)的目的是返回所有 contents
中包含 query
的行。類似于示例 13-19 中的 filter
例子,可以使用 filter
適配器只保留 line.contains(query)
返回 true
的那些行。接著使用 collect
將匹配行收集到另一個 vector 中。這樣就容易多了!嘗試對 search_case_insensitive
函數(shù)做出同樣的使用迭代器方法的修改吧。
接下來的邏輯問題就是在代碼中應(yīng)該選擇哪種風(fēng)格:是使用示例 13-28 中的原始實現(xiàn)還是使用示例 13-29 中使用迭代器的版本?大部分 Rust 程序員傾向于使用迭代器風(fēng)格。開始這有點難以理解,不過一旦你對不同迭代器的工作方式有了感覺之后,迭代器可能會更容易理解。相比擺弄不同的循環(huán)并創(chuàng)建新 vector,(迭代器)代碼則更關(guān)注循環(huán)的目的。這抽象掉那些老生常談的代碼,這樣就更容易看清代碼所特有的概念,比如迭代器中每個元素必須面對的過濾條件。
不過這兩種實現(xiàn)真的完全等同嗎?直覺上的假設(shè)是更底層的循環(huán)會更快一些。讓我們聊聊性能吧。
更多建議: