Rust 改進(jìn)之前的 I/O 項(xiàng)目

2023-03-22 15:12 更新
ch13-03-improving-our-io-project.md
commit cc958ca579816ea6ac7e9067d628b0423a1ed3e4

有了這些關(guān)于迭代器的新知識(shí),我們可以使用迭代器來(lái)改進(jìn)第十二章中 I/O 項(xiàng)目的實(shí)現(xiàn)來(lái)使得代碼更簡(jiǎn)潔明了。讓我們看看迭代器如何能夠改進(jìn) Config::new 函數(shù)和 search 函數(shù)的實(shí)現(xiàn)。

使用迭代器并去掉 clone

在示例 12-6 中,我們?cè)黾恿艘恍┐a獲取一個(gè) String slice 并創(chuàng)建一個(gè) Config 結(jié)構(gòu)體的實(shí)例,他們索引 slice 中的值并克隆這些值以便 Config 結(jié)構(gòu)體可以擁有這些值。在示例 13-24 中重現(xiàn)了第十二章結(jié)尾示例 12-23 中 Config::new 函數(shù)的實(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ù)

那時(shí)我們說(shuō)過(guò)不必?fù)?dān)心低效的 clone 調(diào)用了,因?yàn)閷?lái)可以對(duì)他們進(jìn)行改進(jìn)。好吧,就是現(xiàn)在!

起初這里需要 clone 的原因是參數(shù) args 中有一個(gè) String 元素的 slice,而 new 函數(shù)并不擁有 args。為了能夠返回 Config 實(shí)例的所有權(quán),我們需要克隆 Config 中字段 query 和 filename 的值,這樣 Config 實(shí)例就能擁有這些值。

在學(xué)習(xí)了迭代器之后,我們可以將 new 函數(shù)改為獲取一個(gè)有所有權(quán)的迭代器作為參數(shù)而不是借用 slice。我們將使用迭代器功能之前檢查 slice 長(zhǎng)度和索引特定位置的代碼。這會(huì)明確 Config::new 的工作因?yàn)榈鲿?huì)負(fù)責(zé)訪問(wèn)這些值。

一旦 Config::new 獲取了迭代器的所有權(quán)并不再使用借用的索引操作,就可以將迭代器中的 String 值移動(dòng)到 Config 中,而不是調(diào)用 clone 分配新的空間。

直接使用 env::args 返回的迭代器

打開(kāi) I/O 項(xiàng)目的 src/main.rs 文件,它看起來(lái)應(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ù)的開(kāi)頭為示例 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ù)返回一個(gè)迭代器!不同于將迭代器的值收集到一個(gè) vector 中接著傳遞一個(gè) slice 給 Config::new,現(xiàn)在我們直接將 env::args 返回的迭代器的所有權(quán)傳遞給 Config::new。

接下來(lái)需要更新 Config::new 的定義。在 I/O 項(xiàng)目的 src/lib.rs 中,將 Config::new 的簽名改為如示例 13-26 所示。這仍然不能編譯因?yàn)槲覀冞€需更新函數(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)庫(kù)文檔顯示,它返回的迭代器的類(lèi)型為 std::env::Args。我們已經(jīng)更新了 Config :: new 函數(shù)的簽名,因此參數(shù) args 的類(lèi)型為 std::env::Args 而不是 &[String]。因?yàn)槲覀儞碛?nbsp;args 的所有權(quán),并且將通過(guò)對(duì)其進(jìn)行迭代來(lái)改變 args ,所以我們可以將 mut 關(guān)鍵字添加到 args 參數(shù)的規(guī)范中以使其可變。

使用 Iterator trait 代替索引

接下來(lái),我們將修改 Config::new 的內(nèi)容。標(biāo)準(zhǔn)庫(kù)文檔還提到 std::env::Args 實(shí)現(xiàn)了 Iterator trait,因此我們知道可以對(duì)其調(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ù)體來(lái)使用迭代器方法

請(qǐng)記住 env::args 返回值的第一個(gè)值是程序的名稱。我們希望忽略它并獲取下一個(gè)值,所以首先調(diào)用 next 并不對(duì)返回值做任何操作。之后對(duì)希望放入 Config 中字段 query 調(diào)用 next。如果 next 返回 Some,使用 match 來(lái)提取其值。如果它返回 None,則意味著沒(méi)有提供足夠的參數(shù)并通過(guò) Err 值提早返回。對(duì) filename 值進(jìn)行同樣的操作。

使用迭代器適配器來(lái)使代碼更簡(jiǎn)明

I/O 項(xiàng)目中其他可以利用迭代器的地方是 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ù)的定義

可以通過(guò)使用迭代器適配器方法來(lái)編寫(xiě)更簡(jiǎn)明的代碼。這也避免了一個(gè)可變的中間 results vector 的使用。函數(shù)式編程風(fēng)格傾向于最小化可變狀態(tài)的數(shù)量來(lái)使代碼更簡(jiǎn)潔。去掉可變狀態(tài)可能會(huì)使得將來(lái)進(jìn)行并行搜索的增強(qiáng)變得更容易,因?yàn)槲覀儾槐毓芾?nbsp;results vector 的并發(fā)訪問(wèn)。示例 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ù)實(shí)現(xiàn)中使用迭代器適配器

回憶 search 函數(shù)的目的是返回所有 contents 中包含 query 的行。類(lèi)似于示例 13-19 中的 filter 例子,可以使用 filter 適配器只保留 line.contains(query) 返回 true 的那些行。接著使用 collect 將匹配行收集到另一個(gè) vector 中。這樣就容易多了!嘗試對(duì) search_case_insensitive 函數(shù)做出同樣的使用迭代器方法的修改吧。

接下來(lái)的邏輯問(wèn)題就是在代碼中應(yīng)該選擇哪種風(fēng)格:是使用示例 13-28 中的原始實(shí)現(xiàn)還是使用示例 13-29 中使用迭代器的版本?大部分 Rust 程序員傾向于使用迭代器風(fēng)格。開(kāi)始這有點(diǎn)難以理解,不過(guò)一旦你對(duì)不同迭代器的工作方式有了感覺(jué)之后,迭代器可能會(huì)更容易理解。相比擺弄不同的循環(huán)并創(chuàng)建新 vector,(迭代器)代碼則更關(guān)注循環(huán)的目的。這抽象掉那些老生常談的代碼,這樣就更容易看清代碼所特有的概念,比如迭代器中每個(gè)元素必須面對(duì)的過(guò)濾條件。

不過(guò)這兩種實(shí)現(xiàn)真的完全等同嗎?直覺(jué)上的假設(shè)是更底層的循環(huán)會(huì)更快一些。讓我們聊聊性能吧。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)