改善我們的 I/O 專案
有了疊代器這樣的新知識,我們可以使用疊代器來改善第十二章的 I/O 專案,讓程式碼更清楚與簡潔。我們來看看疊代器如何改善 Config::build
函式與 search
函式的實作。
使用疊代器移除 clone
在範例 12-6 中,我們加了些程式碼來取得 String
數值的切片並透過索引切片與克隆數值來產生 Config
實例,讓 Config
結構體能擁有其數值。在範例 13-17 中,我們重現了範例 12-23 的 Config::build
函式實作:
檔案名稱:src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("引數不足");
}
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,
})
}
}
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(())
}
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
}
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
}
#[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)
);
}
}
當時我們說先不用擔心 clone
呼叫帶來的效率問題,因為我們會在之後移除它們。現在正是絕佳時機!
我們在此需要 clone
的原因為我們的參數 args
是擁有 String
元素的切片,但是 build
函式並不擁有 args
。要回傳 Config
實例的所有權,我們必須克隆數值給 Config
的 query
與 file_path
欄位,Config
實例才能擁有其值。
有了我們新學到的疊代器,我們可以改變 build
函式來取得疊代器的所有權來作為引數,而非借用切片。我們會來使用疊代器的功能,而不是檢查切片長度並索引特定位置。這能讓 Config::build
函式的意圖更清楚,因為疊代器會存取數值。
一旦 Config::build
取得疊代器的所有權並不再使用借用的索引動作,我們就可以從疊代器中移動 String
的數值至 Config
而非呼叫 clone
來產生新的配置。
直接使用回傳的疊代器
請開啟你的 I/O 專案下的 src/main.rs 檔案,這應該會看起來像這樣:
檔案名稱:src/main.rs
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!("解析引數時出現問題:{err}");
process::exit(1);
});
// --省略--
if let Err(e) = minigrep::run(config) {
eprintln!("應用程式錯誤:{e}");
process::exit(1);
}
}
我們首先會改變範例 12-24 的 main
函式開頭段落成範例 13-18 這段使用疊代器的程式碼。這在我們更新 Config::build
之前都還無法編譯。
檔案名稱:src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("解析引數時出現問題:{err}");
process::exit(1);
});
// --snip--
if let Err(e) = minigrep::run(config) {
eprintln!("應用程式錯誤:{e}");
process::exit(1);
}
}
env::args
函式回傳的是疊代器!與其收集疊代器的數值成一個向量再傳遞切片給 Config::build
,現在我們可以直接傳遞 env::args
回傳的疊代器所有權給 Config::build
。
接下來,我們需要更新 Config::build
的定義。在 I/O 專案的 src/lib.rs 檔案中,讓我們變更 Config::build
的簽名成範例 13-19 的樣子。這還無法編譯,因為我們需要更新函式本體。
檔案名稱:src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("引數不足");
}
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,
})
}
}
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(())
}
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
}
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
}
#[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)
);
}
}
標準函式庫技術文件顯示 env::args
函式回傳的疊代器型別為 std::env::Args
,而該疊代器有實作 Iterator
特徵並回傳 String
數值。
我們更新了 Config::build
函式的簽名,讓參數 args
擁有個特徵界限為 impl Iterator<Item = String>
的泛型型別而非 &[String]
。我們在第十章的「特徵作為參數」段落討論過 impl Trait
的語法,這樣的用法讓 args
可以接收任何有實作 Iterator
並回傳 String
數值的型別。
因為我們取得了 args
的所有權,而且我們需要將 args
成為可變的讓我們可以疊代它,所以我們將關鍵字 mut
加到 args
的參數指定使其成為可變的。
使用 Iterator
特徵方法而非索引
接下來,我們要修正 Config::build
的本體。因為 args
有實作 Iterator
特徵,所以我們知道我們可以對它呼叫 next
方法!範例 13-20 更新了範例 12-23 的程式碼來使用 next
方法:
檔案名稱:src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("無法取得搜尋字串"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("無法取得檔案路徑"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
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(())
}
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
}
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
}
#[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)
);
}
}
我們還記得 env::args
回傳的第一個數值會是程式名稱。我們想要忽略該值並取得下個數值,所以我們第一次呼叫 next
時不會對回傳值做任何事。再來我們才會呼叫 next
來取得我們想要的數值置入 Config
中的 query
欄位。如果 next
回傳 Some
的話,我們使用 match
來提取數值。如果它回傳 None
的話,這代表引數不足,所以我們提早用 Err
數值回傳。我們對 file_path
數值也做一樣的事。
透過疊代配接器讓程式碼更清楚
我們也可以對 I/O 專案中的 search
函式利用疊代器的優勢,範例 13-21 重現了範例 12-19 的程式碼:
檔案名稱:src/lib.rs
use std::error::Error;
use std::fs;
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("引數不足");
}
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)?;
Ok(())
}
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
}
#[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));
}
}
我們可以使用疊代配接器(iterator adaptor)方法讓此程式碼更精簡。這樣做也能避免我們產生過程中的 results
可變向量。函式程式設計風格傾向於最小化可變狀態的數量使程式碼更加簡潔。移除可變狀態還在未來有機會讓搜尋可以平行化,因為我們不需要去管理 results
向量的並行存取。範例 13-22 展示了此改變:
檔案名稱:src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("無法取得搜尋字串"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("無法取得檔案路徑"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
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(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
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
}
#[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)
);
}
}
回想一下 search
函式的目的是要回傳 contents
中所有包含 query
的行數。類似於範例 13-16 的 filter
範例,此程式碼使用 filter
配接器來只保留 line.contains(query)
回傳為 true
的行數。我們接著就可以用 collect
收集符合的行數成另一個向量。這樣簡單多了!你也可以對 search_case_insensitive
函式使用疊代器方法做出相同的改變。
迴圈與疊代器之間的選擇
接下來的邏輯問題是在你自己的程式碼中你應該與為何要使用哪種風格呢:是要原本範例 13-21 的程式碼,還是範例 13-22 使用疊代器的版本呢?大多數的 Rust 程式設計師傾向於使用疊代器。一開始的確會有點難上手,不過一旦你熟悉各種疊代配接器與它們的用途後,疊代器就會很好理解了。不同於用迴圈迂迴處理每一步並建構新的向量,疊代器能更專注在迴圈的高階抽象上。這能抽象出常見的程式碼,並能更容易看出程式碼中的重點部位,比如疊代器中每個元素要過濾的條件。
但是這兩種實作真的完全相等嗎?你的直覺可能會假設低階的迴圈可能更快些。讓我們來討論效能吧。