2020/05/11

rust 13 Functional Language Features: Iterators and Closures

函數式編程風格包含將函數作為參數值或其他函數的返回值、將函數賦值給變數以供之後執行等等。

rust 提供兩個 funcational language 的特性

  • 閉包(Closures),一個可以儲存在變數裡的類似函數的結構
  • 迭代器(Iterators),一種處理元素序列的方式

還有先前已經說明的 pattern matching 及 enum,也是受到 funcational language 的影響

Closures: 可獲取環境的 Anonymous Functions

closure 是可儲存變數或作為參數傳遞給其他函數的 anonymous function。可在某處產生 closure,在不同 context 中進行運算,closure 跟 function 不同,允許獲取呼叫者 scope 中的值。

利用 closure 產生抽象行為

假想情境:在一個利用 app 產生的自訂健身計畫的公司工作,後端以 rust 編寫,但產生健身計畫需要考慮很多參數,例如年齡、BMI、喜好、最近健身活動與自訂強度係數。我們希望在需要時呼叫計算的演算法,且計算所需時間很短,只希望呼叫一次,不讓 user 等太久。

先做一個假的計算函數 (simulatedexpensivecalculation),執行 2s,在 main 定義 generateworkout 參數,將業務邏輯寫在 generateworkout 裡面

use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: u32) -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    intensity
}

// 根據輸入呼叫 simulated_expensive_calculation 函數來列印健身計畫
fn generate_workout(intensity: u32, random_number: u32) {
    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)
        );
        println!(
            "Next, do {} situps!",
            simulated_expensive_calculation(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

fn main() {
    // 實際的程式會從 app 前端獲取強度係數並使用 rand crate 來生成亂數,目前先訂為固定值
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

將會對 simulatedexpensivecalculation 進行修改,為了簡化更新步驟,要重構程式,只呼叫 simulatedexpensivecalculation 一次,並去掉目前多餘的兩次函數呼叫。

利用函數重構

將重複的 simulated_expensive_calculation 函數呼叫提取到一個變數中,並將結果儲存在變量 expensive_result

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_result =
        simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result
        );
        println!(
            "Next, do {} situps!",
            expensive_result
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result
            );
        }
    }
}

利用 closure 重構

定義一個 closure 並將其儲存在變數中,可以選擇將整個 simulated_expensive_calculation 函數體移動到這裡引入的 closure 中

let expensive_closure = |num| {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

closure 定義以一對 | 開始,在裡面指定 closure 的參數,這種語法跟 smalltalk, ruby 類似,如果有多個參數,就 | param1, param2 | 這樣定義。

參數後面是 { } 的 closure body,如果裡面只有一行,可省略 {}。最後要加上 ;,因 closure body 最後是 num,跟函數一樣,就是回傳 num

let 表示 expensive_closure 是一個 anonymous function 的定義,不是呼叫 anonymous function 的回傳值。呼叫 closure 類似呼叫函數。

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_closure(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

closure 類別推斷與註解

首先討論一下為何閉包定義中和所涉及的 trait 中沒有類型註解

closure 並不要求像 fn 一樣在參數及回傳值上註明類別。closure 不同於 function,沒有暴露在外面的介面,不需要特別命名或給 library 使用。closure 通常很短,且只與對應任意較小的 context 中,在有限制的 context,compiler 可以自行推測參數及回傳值得的資料型別。

如果像要更明確的 code,還是可以加上類別註解

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

closure 的語法很接近 function

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };   // 省略了類別註解
let add_one_v4 = |x|               x + 1  ;   // 省略 {}

closure 會自行推測類別,但如果呼叫兩次,但給了不同類別,會發生編譯 error

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

使用帶有泛型及 Fn trait 的 closure

改善多次呼叫 closure 的問題,可將結果儲存到變數中。

另外一種方法,是產生一個存放 closure 及呼叫 closure 結果的 struct,該 struct 只會在需要結果時,執行 closure 並儲存結果,這個 pattern 稱為 memorization 或 lazy evaluation

為了讓 struct 存放 closure,需要指定 closure 的類別,因為 struct 定義需要知道其每一個 item 的類別。每一個 closure instance 有其自己獨有的匿名類型:也就是說,即使兩個 closure 有相同的簽名,他們的類別仍然是不同的。為了定義使用 closure 的結構體、枚舉或函數參數,需要像第十章討論的那樣使用泛型和 trait bound。

Fn 系列 trait 由 standard library 提供。所有的 closure 都實現了 trait FnFnMutFnOnce 中的一個。在“閉包會捕獲其環境”部分我們會討論這些 trait 的區別;在這個例子中可以使用 Fn trait。

為了滿足 Fn trait bound 我們增加了代表 closure 所必須的參數和返回值類型的類別。在這個例子中,closure 有一個 u32 的參數並返回一個 u32,這樣所指定的 trait bound 就是 Fn(u32) -> u32

定義 Cacher,在 calculation 儲存 closure,在 value 存放 Option

struct Cacher<T>
    where T: Fn(u32) -> u32
{
    calculation: T,
    value: Option<u32>,
}

Cacher 的 fields 是私有的,因為我們希望 Cacher 管理這些值而不是直接改變他們。

impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}

Cacher::new 函數獲取一個泛型參數 T,它定義於 impl 塊上下文中並與 Cacher 結構體有著相同的 trait bound。Cacher::new 返回一個在 calculation 字段中存放了指定 closure 和在 value 字段中存放了 None 值的 Cacher 實例,因為我們還未執行 closure。

在需要 closure 的執行結果時,不同於直接呼叫 closure,它會呼叫 value 方法。這個方法會檢查 self.value 是否已經有了一個 Some 的結果值;如果有,它返回 Some 中的值並不會再次執行closure。

如果 self.valueNone,則會呼叫 self.calculation 中儲存的closure,將結果保存到 self.value 以便將來使用,並同時返回結果值。

在 generate_workout 中利用 Cacher

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

Cacher 的限制

目前 Cacher 的實現存在兩個小問題,這使得在不同上下文中復用變得很困難。

Cacher 假設對於 value 方法的任何 arg 參數值總是會返回相同的值。也就是說,這個 Cacher 的測試會失敗:

#[test]
fn call_with_different_values() {
    let mut c = Cacher::new(|a| a);

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v2, 2);
}

可修改 Cacher 存放一個 hash map 而不是單獨一個值。hash map 的 key 將是傳遞進來的 arg值,而 value 則是對應 key 呼叫 closure 的結果值。相比之前檢查 self.value 直接是 Some 還是 None 值,現在 value 函數會在hash map 中尋找 arg,如果找到的話就返回其對應的值。如果不存在,Cacher 會呼叫 closure 並將結果值保存在 hash map 對應 arg 值的位置。

第二個問題是它的應用被限制為只接受獲取一個 u32 值並返回一個 u32 值的 closure。比如說,我們可能需要能夠緩存一個獲取字符串 slice 並返回 usize 值的 closure 的結果。請嘗試引入更多泛型參數來增加 Cacher 功能的靈活性。

以 closure 捕獲環境

closure 有另一個函數所沒有的功能:他們可以捕獲其環境並訪問其被定義的作用域的變數。

fn main() {
    let x = 4;

    // x 不是 equal_to_x 的參數,是跟 equal_to_x 相同 scope 的變數
    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

當 closure 從環境中捕獲一個值,closure 會在閉包體中儲存這個值以供使用。這會使用 memory 並產生額外的開銷。

closure 可以通過三種方式捕獲其環境,他們直接對應函數的三種獲取參數的方式:獲取所有權,可變借用和不可變借用。這三種捕獲值的方式被編碼為如下三個 Fn trait:

  • FnOnce 使用從周圍作用域捕獲的變數,closure 周圍的作用域被稱為其 環境environment。為了使用捕獲到的變數,使用必須獲取其所有權並在定義閉包時將其移動進 closure。其名稱的 Once 部分代表了closure不能多次獲取相同變數的所有權的事實,所以它只能被呼叫一次。
  • FnMut 獲取可變的借用值,所以可以改變其環境
  • Fn 從其環境獲取不可變的借用值

產生 closure 時,Rust 根據其如何使用環境中變數來推斷我們希望如何引用環境。由於所有 closure 都可以被呼叫至少一次,所以所有 closure 都實現了 FnOnce 。那些並沒有移動被捕獲變數的所有權到 closure 內的closure也實現了 FnMut ,而不需要對被捕獲的變數進行可變訪問的closure也實現了 Fn。 在示例 13-12 中,equal_to_x closure 不可變的借用了 x(所以 equal_to_x 具有 Fn trait),因為 closure body 只需要讀取 x 的值。

如果你希望強制 closure 獲取其使用的環境值的所有權,可以在參數列表前使用 move 關鍵字。這個技巧在將 closure 傳遞給新線程以便將數據移動到新線程中時最實用。

fn main() {
    // 改為 vector,因為原本的整數,可被 copy 而不是 move
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;

    // 因為 x 被移動到 closure,main 就不被允許使用 x
    // println!("can't use x here: {:?}", x);

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}

大部分需要指定一個 Fn 系列 trait bound 的時候,可以從 Fn 開始,而編譯器會根據 closure body 中的情況告訴你是否需要 FnMutFnOnce

用 iterator 處理元素序列

rust 的 iterator 是惰性 lazy 的,在使用前都不會有效果。例如,下面產生了一個 iterator,但在沒有使用前,沒有任何作用。

let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();

使用 iterator

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {}", val);
}

Iterator trait 和 next 方法

iterator 都實作了 Iterator trait

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 此處省略了方法的預設實作
}

type ItemSelf::Item 是新的語法,定義了 trait 的關聯類別 associated type

Iterator trait 要求同時定義一個 Item 類別,該類別用在 next 的回傳值類別

next 一次回傳一個 item,封裝在 Some 裡面,當 iterator 結束時,會回傳 None

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

注意 v1_iter 必須是可變的,在 iterator 上呼叫 next 方法會改變用來記錄序列位置的狀態。換句話說,代碼 消費(consume)了,或使用了迭代器。每一個 next 呼叫都會從 iterator 中消費一個項。使用 for 循環時不需要使 v1_iter 可變,因為 for 循環會獲取 v1_iter 的所有權並在後台使 v1_iter 可變。

next 呼叫中得到的值是 vector 的不可變引用。iter 方法生成一個不可變引用的 iterator。如果我們需要一個獲取 v1 所有權並返回擁有所有權的 iterator,可以呼叫 into_iter而不是 iter。如果我們希望迭代可變引用,則可以調用 iter_mut 而不是 iter

消費了 iterator 的方法

Iterator trait 有一系列不同的由標準庫提供默認實現的方法;你可以在 Iterator trait 的標準庫 API 文檔中找到所有這些方法。一些方法在其定義中呼叫了 next 方法,這也就是為什麼在實現 Iterator trait 時要求實現 next 方法的原因。

這些呼叫 next 方法的方法被稱為 消費適配器consuming adaptors),因為呼叫他們會消耗 iterator。ex: sum 方法獲取 iterator 的所有權並反覆呼叫 next 來遍歷迭代器,因而會消費迭代器。當其遍歷每一個項時,它將每一個項加總到一個總和並在迭代完成時回傳總和。

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

呼叫 sum 之後不再允許使用 v1_iter 因為呼叫 sum 時它會獲取 iterator 的所有權。

產生其他 iterators 的方法

Iterator trait 中定義了另一類方法,被稱為 迭代器適配器iterator adaptors),可將當前 iterator 變為不同類型的 iterator。可鏈式呼叫多個迭代器適配器,但因為所有的迭代器都是惰性的,必須呼叫一個消費適配器方法以便獲取迭代器適配器呼叫的結果。

示例 13-17 展示了一個調用迭代器適配器方法 map 的例子,該 map 方法使用閉包來調用每個元素以生成新的迭代器。 這裡的閉包創建了一個新的迭代器,對其中 vector 中的每個元素都被加 1。不過這些代碼會產生一個警告:

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);
warning: unused `std::iter::Map` that must be used
 --> src/main.rs:5:2
  |
5 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(unused_must_use)] on by default
  = note: iterators are lazy and do nothing unless consumed

警告提醒了我們為什麼:iterator adaptors 是惰性的,而這裡我們需要消費迭代器。用 collect 將結果收集到一個資料結構中

let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

使用 closure 獲取環境

這是利用 filter iterator adaptor 與捕獲環境的 closure 的例子

iterator 的 filter 方法獲取一個使用 iterator 每一個項並返回 boolean 的 closure。如果closure返回 true,其值將會包含在 filter 提供的新迭代器中。如果 closure 返回 false,其值不會包含在結果迭代器中。

使用 filter 和一個捕獲環境中變量 shoe_size 的閉包,這樣閉包就可以遍歷一個 Shoe struct 集合以便只返回指定大小的鞋子:

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter()
        .filter(|s| s.size == shoe_size)
        .collect()
}

#[test]
fn filters_by_size() {
    let shoes = vec![
        Shoe { size: 10, style: String::from("sneaker") },
        Shoe { size: 13, style: String::from("sandal") },
        Shoe { size: 10, style: String::from("boot") },
    ];

    let in_my_size = shoes_in_my_size(shoes, 10);

    assert_eq!(
        in_my_size,
        vec![
            Shoe { size: 10, style: String::from("sneaker") },
            Shoe { size: 10, style: String::from("boot") },
        ]
    );
}

shoes_in_my_size 函數獲取一個鞋子 vector 的所有權和一個鞋子大小作為參數。它回傳一個只包含指定大小鞋子的 vector。

shoes_in_my_size 函數呼叫了 into_iter 來創建一個獲取 vector 所有權的 iterator。呼叫 filter 將這個迭代器適配成一個只含有那些閉包返回 true 的元素的 new iterator。

closure 從環境中捕獲了 shoe_size 變數並使用其值與每一隻鞋的大小作比較,只保留指定大小的鞋子。最終,調用 collect 將迭代器適配器返回的值收集進一個 vector 並返回。

當呼叫 shoes_in_my_size 時,我們只會得到與指定值相同大小的鞋子。

實作 Iterator trait 建立自訂 iterator

在 vector 呼叫 iter, into_iteritem_mut 可產生 iterator,也可以用 standard library 中其他 collection 類別產生 iterator (ex: hash map)。也可以實作 Iterator trait 產生自訂 iterator,定義中唯一需要的方法是 next

ex: 一個只會從 1 數到 5 的 iterator 。首先,創建一個結構體來存放一些值,接著實現 Iterator trait 將這個結構體放入迭代器中並在此實現中使用其值。

//定義 Counter struct和一個 count 初值為 0 的 Counter 實例的 new 函數
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

// 為 Counter 實現 Iterator trait,通過定義 next 方法來指定使用 iterator 時的行為
impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

#[test]
fn calling_next_directly() {
    let mut counter = Counter::new();

    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}

通過定義 next 方法實現 Iterator trait,我們現在就可以使用任何標準庫定義的擁有默認實現的 Iterator trait 方法了,因為他們都使用了 next 方法的功能。

ex: 獲取 Counter 實例產生的值,將這些值與另一個 Counter 實例在省略了第一個值之後產生的值配對,將每一對值相乘,只保留那些可以被三整除的結果,然後將所有保留的結果相加

#[test]
fn using_other_iterator_trait_methods() {
    let sum: u32 = Counter::new().zip(Counter::new().skip(1))
                                 .map(|(a, b)| a * b)
                                 .filter(|x| x % 3 == 0)
                                 .sum();
    assert_eq!(18, sum);
}

注意: zip 只會產生 (1,2) (2,3) (3,4) (4,5) 沒有 (5, None)

改良 I/O project

使用 iterator 改進 Config:newsearch

原始程式 lib.rs

use std::error::Error;
use std::fs;
use std::env;

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

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        // 使用 env 環境變數
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

// 需要在 search 的簽名中定義一個顯式生命週期 'a 並用於 contents 參數和返回值。
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    // 用 lines 處理每一行資料,包含 query 時,就放到 results
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// 在比較查詢和每一行之前將他們都轉換為小寫
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
}

// 使用 "duct" 作為這個測試中需要搜索的字符串。用來搜索的文本有三行,其中只有一行包含 "duct"。我們斷言 search 函數的返回值只包含期望的那一行。
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

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

main.rs

use std::env;
use std::process;
use minigrep;
use minigrep::Config;

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

利用 iterator 去掉 clone

Config 原本是用 clone 取得 command line 參數

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        // 使用 env 環境變數
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

可將 new 改為獲取一個有所有權的 iterator 為參數,而不是借用 slice

Config::new 獲取 iterator 的所有權,並不再使用 借用 的索引操作,就可將 iterator 的 String 移動到 Config 中

直接使用 env:args 的 iterator

修改 main,將 env::args 的返回值傳給 Config::new

fn main() {
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--
}

更新 Config::new 接受一個 iterator,env::args 函數的標準庫文檔展示了其返回的迭代器類型是 std::env::Args。因為這裡需要獲取 args 的所有權且通過迭代改變 args,我們可以在 args 參數前指定 mut 關鍵字使其可變。

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        // --snip--

使用 Iterator trait 方法代替索引

Config::new 改為呼叫 args.next()

impl Config {
    pub fn new(mut args: std::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 })
    }
}

利用 iterator adaptor 簡化程式

原本 search 的寫法是

fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    // 用 lines 處理每一行資料,包含 query 時,就放到 results
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

改用iterator,可避免使用可變的 results vector,就能避免 concurrent 存取 results 的問題

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents.lines()
        .filter(|line| line.contains(query))
        .collect()
}

大部分 Rust programmer 傾向於使用 iterator,一旦你對不同 iterator 的工作方式有了感覺之後,迭代器可能會更容易理解。相比使用不同的循環並產生新 vector,iterator codes 更關注循環的目的。

比較 loops 與 iterators 的效能

用 iterator 會比 for 的程式速度快一點

iterator 在 rust 是 zero-cost abstraction,抽象但不會增加執行的 overhead

ex: 有三個變數:一個叫 buffer 的數據 slice、一個有 12 個元素的數組 coefficients、和一個代表位移位數的 qlp_shift 。為了計算 prediction 的值,這些代碼遍歷了 coefficients 中的 12 個值,使用 zip 方法將係數與 buffer 的前 12 個值組合在一起。接著將每一對值相乘,再將所有結果相加,然後將總和右移 qlp_shift 位。

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

Rust 知道這裡會迭代 12 次,所以它會“展開”(unroll)循環。展開是一種移除循環程式開銷,並替換為每個迭代中的重複代碼的優化。

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言