改善我們的 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)
        );
    }
}

範例 13-17:重現範例 12-23 的 Config::build 函式

當時我們說先不用擔心 clone 呼叫帶來的效率問題,因為我們會在之後移除它們。現在正是絕佳時機!

我們在此需要 clone 的原因為我們的參數 args 是擁有 String 元素的切片,但是 build 函式並不擁有 args。要回傳 Config 實例的所有權,我們必須克隆數值給 Configqueryfile_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);
    }
}

範例 13-18:傳遞 env::args 的回傳值給 Config::build

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)
        );
    }
}

範例 13-19:更新 Config::build 的簽名來接收疊代器

標準函式庫技術文件顯示 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)
        );
    }
}

範例 13-20:變更 Config::build 的本體來使用疊代器方法

我們還記得 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));
    }
}

範例 13-21:範例 12-19 的 search 函式實作

我們可以使用疊代配接器(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)
        );
    }
}

範例 13-22:對 search 函式實作使用疊代配接器方法

回想一下 search 函式的目的是要回傳 contents 中所有包含 query 的行數。類似於範例 13-16 的 filter 範例,此程式碼使用 filter 配接器來只保留 line.contains(query) 回傳為 true 的行數。我們接著就可以用 collect 收集符合的行數成另一個向量。這樣簡單多了!你也可以對 search_case_insensitive 函式使用疊代器方法做出相同的改變。

迴圈與疊代器之間的選擇

接下來的邏輯問題是在你自己的程式碼中你應該與為何要使用哪種風格呢:是要原本範例 13-21 的程式碼,還是範例 13-22 使用疊代器的版本呢?大多數的 Rust 程式設計師傾向於使用疊代器。一開始的確會有點難上手,不過一旦你熟悉各種疊代配接器與它們的用途後,疊代器就會很好理解了。不同於用迴圈迂迴處理每一步並建構新的向量,疊代器能更專注在迴圈的高階抽象上。這能抽象出常見的程式碼,並能更容易看出程式碼中的重點部位,比如疊代器中每個元素要過濾的條件。

但是這兩種實作真的完全相等嗎?你的直覺可能會假設低階的迴圈可能更快些。讓我們來討論效能吧。