iT邦幫忙

2024 iThome 鐵人賽

DAY 13
1
自我挑戰組

從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用系列 第 13

[Day 13] 使用迭代器與集合:Rust 中常見的集合處理

  • 分享至 

  • xImage
  •  

在 Rust 中,迭代器和集合是不可或缺的工具,特別是在處理大量資料時。Rust 提供了各種集合類別(如 Vec, HashMap, HashSet 等)和強大的迭代器機制,使我們能夠高效地處理資料。這篇文章將深入探討迭代器的運作方式,並展示如何使用集合來處理資料。


什麼是迭代器?

迭代器是一個能夠依次返回集合中的每個元素的物件。它是 Rust 的一個強大功能,因為它允許你以高效、延遲執行的方式處理資料。Rust 的迭代器還具有鏈式方法呼叫的特性,這使得編寫簡潔且高效的程式碼變得更容易。

迭代器的三個核心特性:

  1. 延遲執行:迭代器不會立即執行操作,只有在需要時才會執行,這使得它們在處理大量資料時非常高效。例如,像 map()filter() 這類操作不會立刻產生結果,只有在最終需要結果時(如使用 collect() 收集)才會執行,避免不必要的計算。

  2. 鏈式方法呼叫:可以將多個操作連續應用在迭代器上,讓代碼看起來更簡潔。每一個操作如同組裝積木,將不同的處理步驟串聯起來,不但增強了程式碼的可讀性,還能簡化邏輯。

  3. 所有權與借用的安全性:迭代器能確保記憶體管理的安全性,防止資料競爭。例如,迭代器默認不會消耗集合的所有權,而是借用集合中的元素,這確保了原始集合的完整性和安全性。

讓我們看看一個簡單的範例,來了解如何在 Rust 中使用迭代器:

fn main() {
    // 創建一個數字的 Vec 集合,這是我們要處理的資料
    let numbers = vec![1, 2, 3, 4, 5];
    
    // 使用迭代器過濾出偶數,並將它們加倍
    // 1. `iter()` 方法創建一個迭代器,這是一個對 `numbers` 集合的不可變借用
    // 2. `filter()` 方法接受一個閉包作為參數,用來判斷每個元素是否符合條件
    //    在這裡,`filter()` 過濾出所有的偶數元素
    // 3. `map()` 方法再次接受一個閉包,對符合條件的元素進行處理
    //    在這裡,我們將每個偶數加倍
    // 4. `collect()` 方法將結果收集到一個新的 Vec 集合中
    let doubled_even_numbers: Vec<i32> = numbers
        .iter() // 創建迭代器,這裡返回 `&i32` 型別的元素
        .filter(|&&x| x % 2 == 0) // 過濾出偶數。`&&x` 代表解引用取得 `i32` 值
        .map(|&x| x * 2) // 將每個偶數加倍。`&x` 解引用取得 `i32` 值進行運算
        .collect(); // 收集所有結果到 Vec 中

    // 打印結果,顯示加倍的偶數
    println!("加倍的偶數: {:?}", doubled_even_numbers);
}

結果:

加倍的偶數: [4, 8]

詳細說明:

當我們看到這段程式碼中的 filter(|&&x| x % 2 == 0)map(|&x| x * 2) 時,可以把它們理解為「逐步篩選」和「處理數值」的過程。其中.filter().map()的()裡面都需要加入一個處理的函數來搭配使用,而這個範例當中使用的是閉包

什麼是閉包

在 Day 4 的文章中我們也曾經提到過迭代器,其中展示過.filter()當中需要代入閉包的概念,現在我們再詳細說明一下何謂閉包。

閉包又稱為匿名函數,所以它的本質其實是具有函數功能性的語法,閉包的結構為 |參數| 表達式 ,然而與函數不同的是,閉包可以直接將上下文中已知的變數納入表達式當中,舉例:

let x = 10;
let add = |y| x + y; // 這是一個閉包,捕獲了外部變數 x

println!(add(5)); // 輸出 15

上述的add為一個閉包範例,如果我們使用一般的函數來表達的話,則會遇到一個問題是,x就會也會需要被帶入到函數內,範例如下:

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let x = 10;
    let result = add(x, 5); // 必須明確傳入 x 和 y
    println!("{}", result); // 輸出 15
}

好的,拉回來說明迭代器的範例,程式碼當中:

  1. iter()
    這個方法創建一個迭代器,它是對集合的借用,因此不會改變或消耗原始的集合內容。對於每個元素,它返回一個不可變的引用 &i32,這樣就保證了原始集合的安全性。

  2. .filter(|&&x| x % 2 == 0)

這段程式碼的目的是篩選出 numbers 中的偶數,運作的細節如下:

  • filter() 的作用filter() 是用來過濾掉不符合條件的元素,只保留符合條件的元素。在這裡,我們希望保留所有的偶數。
  • |&&x| 的意思:這裡的 |&&x| 是閉包的參數形式,表示我們正在處理的是一個「引用的引用」。
    • iter() 會返回一個指向 i32 值的引用(&i32)。
    • 由於 filter() 傳給閉包的參數也是引用,所以閉包接收到的是 &&i32,也就是「引用的引用」。
    • &&x 的寫法就是解開兩層引用,讓我們能夠拿到真正的數值 i32,因為在 .iter() 迭代器下,傳入的參數是 &&i32,所以如果只使用 &x 僅解開一層引用,會導致編譯錯誤。
    • x % 2 == 0 的意思:解開 &&x 之後,我們得到真正的數字 x,接著檢查這個數字是否是偶數 (x % 2 == 0),如果是偶數,就保留下來。

這部分的程式碼在做的事情就是從數字中挑選出偶數,因為我們操作的是引用,所以用了 &&x 來解開多層的引用拿到真正的數字值。

  1. .map(|&x| x * 2)

這段程式碼的目的是將篩選出來的偶數進行加倍,運作的細節如下:

  • map() 的作用map() 是用來對每個符合條件的元素進行處理。在這裡,我們想要將每個偶數加倍。
  • |&x| 的意思:因為 filter() 傳遞給 map() 的仍然是元素的引用 &i32,所以 map() 接收到的是 &x
    • &x 是單層引用,即 &i32。用 &x 的方式解開這個引用,就能獲得數值 i32
  • x * 2 的意思x 是實際的數值,這裡對 x 做加倍運算。

這部分的程式碼的目的是把篩選出的偶數加倍。因為 map() 仍然操作的是引用,所以我們使用 &x 解開一層引用,然後對拿到的數值進行加倍。

  1. collect():這個方法最終將所有結果收集到一個新集合中,在此例中是一個 Vec<i32>collect() 是迭代器操作的結束點,這是實際執行前面所有操作的地方。

常見的集合類別

Rust 提供了許多內建的集合類別來處理不同類型的資料需求。以下是幾個常見的集合類別:

1. Vec(動態數組)

Vec 是 Rust 中最常用的集合之一,它是一個動態長度的數組,能夠在運行時擴展或縮小。可以用來儲存相同類型的資料。例如,你想要建立一個可以隨時新增水果的清單:

fn main() {
    let mut fruits = vec!["apple", "banana", "cherry"];
    fruits.push("orange"); // 新增元素
    println!("{:?}", fruits);
}

** vec! **:這裡的 vec! 是一個巨集,用於快速建立一個 Vec(動態數組),在 Rust 當中巨集使用通常都是全小寫加上結尾的驚嘆號,如 vec!println!format! 等。

Vec 常見操作

  • push(item):將元素加入 Vec 的尾部。
  • pop():移除並返回 Vec 的最後一個元素。
  • insert(index, item):在指定索引處插入元素。
  • remove(index):移除指定索引的元素。
  • len():返回 Vec 的長度。
  • is_empty():檢查 Vec 是否為空。
  • clear():清除所有元素。

2. HashMap(雜湊映射)

HashMap 是一種鍵值對(key-value)的資料結構,類似於 Python 的字典。它適合用來儲存需要快速查找的資料。例如,你在記錄每個玩家的分數:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 10);
    scores.insert("Bob", 15);

    if let Some(score) = scores.get("Alice") {
        println!("Alice 的分數是: {}", score);
    }
}
  • HashMap 是 Rust 標準庫中常用的集合類型,用於存儲鍵值對(key-value pairs),類似於 Python 的字典(dict)。
  • use std::collections::HashMap; 是將 HashMap 引入當前程式,這是因為 HashMap 位於 Rust 原生的 collections 模組中。
  • 就如同 vec! 一樣,我們可以透過第三方的套件 maplit 所提供的 hashmap!巨集來快速建立 HashMap

Cargo.toml

[dependencies]
maplit = "1.0"

透過 hashmap! 來建立

use maplit::hashmap;

fn main() {
    // 使用 hashmap! 巨集快速建立 HashMap
    let scores = hashmap! {
        "Alice" => 10,
        "Bob" => 15,
    };

    println!("{:?}", scores); // 輸出 {"Alice": 10, "Bob": 15}
}

如果使用 Rust 原生的 collection 也有 HashMap::from 的方法來建立

use std::collections::HashMap;

fn main() {
    // 使用 HashMap::from 直接建立
    let scores = HashMap::from([
        ("Alice", 10),
        ("Bob", 15),
    ]);

    println!("{:?}", scores); // 輸出 {"Alice": 10, "Bob": 15}
}

因此可以按照自己的編寫習慣來選擇建立方式

了解!以下是 VecHashMap 的一些常見操作指令,簡單列出來供參考:

HashMap 常見操作

  • insert(key, value):插入或更新一個鍵值對。
  • get(&key):根據鍵查找對應的值,返回 Option<&V>
  • remove(&key):移除指定鍵的鍵值對。
  • contains_key(&key):檢查是否包含指定的鍵。
  • len():返回 HashMap 中的鍵值對數量。
  • is_empty():檢查 HashMap 是否為空。
  • clear():清除所有鍵值對。

3. HashSet(雜湊集合)

HashSet 是一種不允許重複元素的集合,它適合用來檢查唯一性或進行集合操作。這就與 Python 的 set 具有相同的性質。

use std::collections::HashSet;

fn main() {
    let mut unique_numbers = HashSet::new();
    unique_numbers.insert(10);
    unique_numbers.insert(20);
    unique_numbers.insert(10); // 重複元素不會加入

    println!("唯一數字: {:?}", unique_numbers);
}
  1. HashSet 的基本特性

    • HashSet 是一種無序的集合,這意味著元素沒有固定的順序。
    • HashSet 內部是基於哈希表實現的,因此插入和查詢的時間複雜度平均為 O(1),非常高效。
    • 重複的元素會自動被忽略,因此可以用來快速確保元素的唯一性。
  2. 常用操作方法

    • insert():插入一個元素。如果元素已存在,則不會改變集合,並返回 false 表示插入失敗。
    • remove():移除一個元素,返回 true 表示成功移除,false 表示元素不存在。
    • contains():檢查集合中是否包含某個元素,返回 truefalse
    • len():返回集合中元素的個數。
    • is_empty():檢查集合是否為空。

    範例:

    use std::collections::HashSet;
    
    fn main() {
        let mut fruits = HashSet::new();
        fruits.insert("apple");
        fruits.insert("banana");
        fruits.insert("cherry");
    
        println!("水果集合: {:?}", fruits);
        println!("是否包含 apple? {}", fruits.contains("apple"));
        fruits.remove("banana");
        println!("移除後的集合: {:?}", fruits);
        println!("集合大小: {}", fruits.len());
    }
    
  3. 集合操作

    • HashSet 支援常見的集合操作,例如聯集、交集、差集等,這使得它特別適合處理需要進行集合運算的場景。

    • 聯集(union):合併兩個集合的所有元素,不包含重複值。

    • 交集(intersection):僅保留兩個集合之間的共同元素。

    • 差集(difference):保留在第一個集合中,但不在第二個集合中的元素。

    • 對稱差集(symmetric_difference):保留兩個集合中不重疊的元素。

    範例:

    use std::collections::HashSet;
    
    fn main() {
        let set_a: HashSet<_> = [1, 2, 3, 4].iter().cloned().collect();
        let set_b: HashSet<_> = [3, 4, 5, 6].iter().cloned().collect();
    
        // 聯集
        let union: HashSet<_> = set_a.union(&set_b).cloned().collect();
        println!("聯集: {:?}", union); // 輸出: {1, 2, 3, 4, 5, 6}
    
        // 交集
        let intersection: HashSet<_> = set_a.intersection(&set_b).cloned().collect();
        println!("交集: {:?}", intersection); // 輸出: {3, 4}
    
        // 差集
        let difference: HashSet<_> = set_a.difference(&set_b).cloned().collect();
        println!("差集: {:?}", difference); // 輸出: {1, 2}
    
        // 對稱差集
        let symmetric_difference: HashSet<_> = set_a.symmetric_difference(&set_b).cloned().collect();
        println!("對稱差集: {:?}", symmetric_difference); // 輸出: {1, 2, 5, 6}
    }
    
  4. 實際應用場景

    • 檢查唯一性:例如,檢查用戶輸入的資料是否重複、快速過濾掉重複元素等。
    • 集合運算:如比較不同用戶組之間的共通權限,處理大型資料集的聯集或交集操作等。
    • 去除重覆操作:從大量資料中去除重複項,保留唯一元素。
    • 查找操作:快速查找某個元素是否存在於集合中,效率高於使用 Vec

使用迭代器處理集合

範例:計算平均數

假設我們有一組分數,想要計算這些分數的平均值。我們可以使用迭代器來完成這個任務,這樣的寫法既簡潔又清晰:

fn main() {
    let scores = vec![90, 85, 78, 92, 88];

    let total: i32 = scores.iter().sum();  // 使用迭代器計算總和
    let count = scores.len();  // 獲取數量
    let average = total as f64 / count as f64;  // 計算平均數

    println!("平均分數: {:.2}", average);
}

結果:

平均分數: 86.60

在這裡,我們使用 iter() 來創建迭代器,並通過 sum() 方法計算所有分數的總和。然後我們使用 len() 獲取數量,並計算平均值。

範例:使用 HashMap 統計詞頻

接下來,我們來看一個稍微複雜一點的例子,統計一段文字中的詞頻。這樣的操作在文字分析和自然語言處理中非常常見:

use std::collections::HashMap;

fn main() {
    let text = "hello world hello rust rust rust";

    let mut word_count = HashMap::new();

    for word in text.split_whitespace() {
        let count = word_count.entry(word).or_insert(0);
        *count += 1;
    }

    println!("詞頻統計: {:?}", word_count);
}

結果:

詞頻統計: {"hello": 2, "world": 1, "rust": 3}

在這個範例中,我們使用 split_whitespace() 來分割文字,並且使用 HashMap 來儲存每個單字的出現次數。or_insert(0) 的作用是如果該單字不在 HashMap 中,則插入並初始化其計數為 0,然後每次遇到該單字時加 1。


自訂迭代器

除了使用內建的迭代器,Rust 還允許我們自訂迭代器。自訂迭代器是通過實作 Iterator trait 來完成的。這在一些需要特殊迭代行為的情境中特別有用:

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> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter::new();

    while let Some(value) = counter.next() {
        println!("計數: {}", value);
    }
}

結果:

計數: 1
計數: 2
計數: 3
計數: 4
計數: 5

在這個例子中,我們定義了一個名為 Counter 的結構體,並為它實作了 Iterator trait。每次調用 next() 方法時,計數器會遞增,直到達到 5。這就像你自己定製了一個小工具,可以精確控制它的行為。


總結

迭代器與集合是開發當中不可或缺的操作步驟,畢竟寫程式就是希望透過程式的批量處理方式來進行多重複雜的判斷或者處理程序,通過迭代器的多種資料結構來解決不同的需求,例如 Vec 用於動態數組,HashMap 用於鍵值對資料,HashSet 用於儲存唯一值等,對於熟悉一種程式語言而言是必要的課題。


上一篇
[Day 12] Cargo:Rust 的建置工具與套件管理器
下一篇
[Day 14] Rust 中的模式匹配:match 的進階應用
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言