使用疊代器來處理一系列的項目

疊代器(Iterator)模式讓你可以對 一個項目序列依序進行某些任務。一個疊代器負責遍歷序每個項目以及序列何時結束的邏輯。當你使用疊代器,你不需要自己實作這些邏輯。

在 Rust 中疊代器是惰性(lazy) 的,代表除非你呼叫方法來使用疊代器,不然它們不會有任何效果。舉例來說,範例 13-13 的程式碼會透過 Vec<T> 定義的方法 iter 從向量v1 建立一個疊代器來遍歷它的項目。此程式碼本身沒有啥實用之處。

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

    let v1_iter = v1.iter();
}

範例 13-13:建立一個疊代器

一旦我們建立了疊代器,我們可以有很多使用它的方式。在第三章的範例 3-5 中,我們在 for 迴圈中使用疊代器來對每個項目執行一些程式碼,雖然我們當時沒有詳細解釋 iter 是在做什麼。

範例 13-14 區隔了疊代器的建立與使用疊代器 for 迴圈。疊代器儲存在變數 v1_iter,且在此時沒有任何遍歷的動作發生。當使用 v1_iter 疊代器的 for 迴圈被呼叫時,疊代器中的每個元素才會在迴圈中每次疊代中使用,以此印出每個數值。

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

    let v1_iter = v1.iter();

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

範例 13-14:在 for 迴圈使用疊代器

在標準函式庫沒有提供疊代器的語言中,你可能會用別種方式寫這個相同的函式,像是先從一個變數 0 作為索引開始、使用該變數索引向量來獲取數值,然後在迴圈中增加變數的值直到它抵達向量的總長。

疊代器會為你處理這些所有邏輯,減少重複且你可能會搞砸的程式碼。疊代器還能讓你靈活地將相同的邏輯用於不同的序列,而不只是像向量這種你能進行索引的資料結構。讓我們研究看看疊代器怎麼辦到的。

Iterator 特徵與 next 方法

所有的疊代器都會實作定義在標準函式庫的 Iterator 特徵。特徵的定義如以下所示:


#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

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

    // 以下省略預設實作
}
}

注意到此定義使用了一些新的語法:type ItemSelf::Item,這是此特徵定義的關聯型別(associated type)。我們會在第十九章進一步探討關聯型別。現在你只需要知道此程式碼表示要實作 Iterator 特徵的話,你還需要定義 Item 型別,而此 Item 型別會用在方法 next 的回傳型別中。換句話說,Item 型別會是從疊代器回傳的型別。

Iterator 型別只要求實作者定義一個方法:next 方法會用 Some 依序回傳疊代器中的每個項目,並在疊代器結束時回傳 None

我們可以直接在疊代器呼叫 next 方法。範例 13-15 展示從向量建立的疊代器重複呼叫 next 每次會得到什麼數值。

檔案名稱:src/lib.rs

#[cfg(test)]
mod tests {
    #[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);
    }
}

fn main() {}

範例 13-15:對疊代器呼叫 next 方法

注意到 v1_iter 需要是可變的:在疊代器上呼叫 next 方法會改變疊代器內部用來紀錄序列位置的狀態。換句話說,此程式碼消耗或者說使用了疊代器。每次 next 的呼叫會從疊代器消耗一個項目。而我們不必在 for 迴圈指定 v1_iter 為可變是因為迴圈會取得 v1_iter 的所有權並在內部將其改為可變。

另外還要注意的是我們從 next 呼叫取得的是向量中數值的不可變引用。iter 方法會從疊代器中產生不可變引用。如果我們想要一個取得 v1 所有權的疊代器,我們可以呼叫 into_iter 而非 iter。同樣地,如果我們想要遍歷可變引用,我們可以呼叫 iter_mut 而非 iter

消耗疊代器的方法

標準函式庫提供的 Iterator 特徵有一些不同的預設實作方法,你可以查閱標準函式庫的 Iterator 特徵 API 技術文件來找到這些方法。其中有些方法就是在它們的定義呼叫 next 方法,這就是為何當你實作 Iterator 特徵時需要提供 next 方法的實作。

會呼叫 next 的方法被稱之為消耗配接器(consuming adaptors),因為呼叫它們會使用掉疊代器。其中一個例子就是方法 sum,這會取得疊代器的所有權並重複呼叫 next 來遍歷所有項目,因而消耗掉疊代器。隨著遍歷的過程中,他會將每個項目加到總計中,並在疊代完成時回傳總計數值。範例 13-16 展示了一個使用 sum 方法的測試:

檔案名稱:src/lib.rs

#[cfg(test)]
mod tests {
    #[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);
    }
}

fn main() {}

範例 13-16:呼叫 sum 方法來取得疊代器中所有項目的總計數值

我們在呼叫 sum 之後就不允許使用 v1_iter 因為 sum 取得了疊代器的所有權。

產生其他疊代器的方法

而其他定義在 Iterator 特徵的方法則叫做疊代配接器(iterator adaptors),它們能讓你變更疊代器成其他種類的疊代器。你可以串接數個疊代配接器的呼叫來組織一系列複雜的動作並仍能保持閱讀性。不過因為所有的疊代器都是惰性的,你需要呼叫一個消耗配接器方法來取得疊代配接器呼叫的結果。

範例 13-17 呼叫了疊代器的疊代配接器方法 map,這可以取得一個閉包來對每個項目進行處理以產生一個新的疊代器。閉包會將向量中的每個項目加 1 來產生新的疊代器。不過此程式碼會產生一個警告:

檔案名稱:src/main.rs

fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}

範例 13-17:呼叫疊代配接器 map 來建立新的疊代器

我們獲得的警告如以下所示:

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

    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

範例 13-17 的程式碼不會做任何事情,我們指定的閉包沒有被呼叫到半次。警告提醒了我們原因:疊代配接器是惰性的,我們必須在此消耗疊代器才行。

要修正並消耗此疊代器,我們將使用 collect 方法,這是我們在範例 12-1 搭配 env::args 使用的方法。此方法會消耗疊代器並收集結果數值至一個資料型別集合。

在範例 13-18 中,我們將遍歷 map 呼叫所產生的疊代器結果數值收集到一個向量中。此向量最後會包含原本向量每個項目都加 1 的數值。

檔案名稱:src/main.rs

fn main() {
    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]);
}

範例 13-18:呼叫方法 map 來建立新的疊代器並呼叫 collect 方法來消耗新的疊代器來產生向量

因為 map 接受一個閉包,我們可以對每個項目指定任何我們想做的動作。這是一個展示如何使用閉包來自訂行為,同時又能重複使用 Iterator 特徵提供的遍歷行為的絕佳例子。

使用閉包獲取它們的環境

現在我們介紹了疊代器,我們可以展示一個透過使用 filter 疊代配接器與閉包獲取它們環境的常見範例。疊代器中的 filter 方法會接受一個使用疊代器的每個項目並回傳布林值的閉包。如果閉包回傳 true,該數值就會被包含在 filter 產生的疊代器中;如果閉包回傳 false,該數值就保不會被包含在結果疊代器中。

在範例 13-19 中我們使用 filter 與一個從它的環境獲取變數 shoe_size 的閉包來遍歷一個有 Shoe 結構體實例的集合。它會回傳只有符合指定大小的鞋子:

檔案名稱:src/lib.rs

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("運動鞋"),
            },
            Shoe {
                size: 13,
                style: String::from("涼鞋"),
            },
            Shoe {
                size: 10,
                style: String::from("靴子"),
            },
        ];

        let in_my_size = shoes_in_my_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("運動鞋")
                },
                Shoe {
                    size: 10,
                    style: String::from("靴子")
                },
            ]
        );
    }
}

fn main() {}

範例 13-19:使用 filter 方法與一個獲取 shoe_size 的閉包

函式 shoes_in_my_size 會取得鞋子向量的所有權以及一個鞋子大小作為參數。它會回傳只有符合指定大小的鞋子向量。

shoes_in_my_size 的本體中,我們呼叫 into_iter 來建立一個會取得向量所有權的疊代器。然後我們呼叫 filter 來將該疊代器轉換成只包含閉包回傳為 true 的元素的新疊代器。

閉包會從環境獲取 shoe_size 參數並比較每個鞋子數值的大小,讓只有符合大小的鞋子保留下來。最後呼叫 collect 來收集疊代器回傳的數值進一個函式會回傳的向量。

此測試顯示了當我們呼叫 shoes_in_my_size 時,我們會得到我們指定相同大小的鞋子。

透過 Iterator 特徵建立我們自己的疊代器

我們已經顯示了你可以對向量呼叫 iterinto_iteriter_mut 來建立疊代器。你也可以從標準函式庫的其他集合型別產生疊代器,像是雜湊映射等等。你也可以透過對你自己的型別實作 Iterator 特徵來建立任何你所希望的疊代器。如同之前提到的,你唯一需要提供的方法定義就是 next 方法。一旦你完成,你就可以使用 Iterator 特徵提供的所有預設實作方法!

作為展示,讓我們建立一個只會從 1 數到 5 的疊代器。首先,我們要建立個擁有一些數值的結構體。然後我們對此結構體實作 Iterator 特徵將它變成一個疊代器,並在實作中使用其值。

範例 13-20 有個結構體 Counter 的定義以及能夠產生 Counter 實例的關聯函式 new

檔案名稱:src/lib.rs

struct Counter {
    count: u32,
}

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

fn main() {}

範例 13-20:定義結構體 Counter 與關聯函式 new,這能建立一個初始值 count 為 0 的 Counter 結構體

Counter 結構體只有一個欄位 count,此欄位擁有一個 u32 數值來追蹤我們遍歷 1 到 5 的當前位置。count 欄位是私有的,因為我們希望 Counter 的實作會管理此數值。函式 new 強制建立新實例的行為永遠會從 count 欄位為 0 時開始。

接下來我們對我們的 Counter 型別實作 Iterator 特徵,定義 next 方法本體來指定疊代器的使用行為,如範例 13-21 所示:

檔案名稱:src/lib.rs

struct Counter {
    count: u32,
}

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

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {}

範例 13-21:在我們的 Counter 結構體實作 Iterator 特徵

我們將疊代器的關聯型別 Item 設為 u32,代表疊代器將會回傳 u32 數值。一樣先別擔心關聯型別,我們會在第十九章討論到。

我們希望我們的疊代器對目前的狀態加 1,所以我們將 count 初始化為 0,這樣它就會先回傳 1。如果 count 的值小於 5,next 就會增加 count 的值並用 Some 回傳目前數值。一旦 count 等於 5,我們的疊代器就會停止增加 count 並永遠回傳傳 None

使用 Counter 疊代器的 next 方法

一旦我們實作了 Iterator 特徵,我們就有一個疊代器了!範例 13-22 的測試展示我們可以對我們的 Counter 結構體直接呼叫 next 方法來使用疊代器的功能,就像我們在範例 13-15 對向量建立的疊代器使用的方式一樣。

檔案名稱:src/lib.rs

struct Counter {
    count: u32,
}

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

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

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

fn main() {}

範例 13-22:測試 next 方法實作的功能

此測試建立了一個新的 Counter 實例給變數 counter 並重複呼叫 next,驗證我們實作的疊代器是否行為如我們預期的一樣:回傳數值 1 到 5。

使用其他 Iterator 特徵方法

我們透過定義 next 方法來實作 Iterator 特徵,所以我們現在可使用在標準函式庫提供的 Iterator 特徵中所任何有預設實作的方法了,因為它們都使用到了 next 的方法功能。

舉例來說,如果我們因為某些原因想要取得一個 Counter 實例的數值與另一個 Counter 實例去掉第一個值的數值來做配對、對每個配對相乘、保留結果可以被 3 整除的值,最後將所有結果數值相加,我們可以這樣寫,如範例 13-23 所示:

檔案名稱:src/lib.rs

struct Counter {
    count: u32,
}

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

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

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

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

fn main() {}

範例 13-23:對我們的 Counter 疊代器使用各式各樣的 Iterator 特徵方法

注意到 zip 只會產生四個配對,理論上的 (5, None) 配對是不會產生出來的,因為 zip 會在它的其中一個輸入疊代器回傳 None 時就回傳 None

這些所有呼叫都是可行的,因為我們已經定義了 next 運作的行為,而標準函式庫會提供其他呼叫 next 方法的預設實作。