iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Rust

大家一起跟Rust當好朋友吧!系列 第 15

Day 15: 閉包 (Closures) 與迭代器 (Iterators):函數式程式設計的優雅之道

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十五天!

恭喜你!我們已經順利完成了前兩週的基礎學習,從 Rust 的語法基礎到測試框架,從所有權系統到智慧指標。今天我們要開始第三週的學習旅程,探討一個讓 Rust 程式碼變得更加優雅和高效的主題:閉包 (Closures) 與迭代器 (Iterators)

如果說前兩週我們學的是如何「正確地」寫 Rust 程式碼,那麼今天要學的就是如何「優雅地」寫 Rust 程式碼。閉包和迭代器是函數式程式設計的重要概念,它們讓我們能夠以更簡潔、更表達性的方式處理資料和邏輯。

老實說,剛開始接觸函數式程式設計時,我覺得這些語法看起來很「炫」但不太實用。但隨著深入使用,我發現閉包和迭代器不僅讓程式碼更易讀,還能帶來更好的效能!今天讓我們一起探索這個讓 Rust 程式碼變得如詩如畫的美妙世界。

閉包 (Closures):捕獲環境的匿名函式

什麼是閉包?

閉包是可以捕獲其環境中變數的匿名函式。你可以把它想像成「會記住周圍環境的函式」:

fn main() {
    let x = 4;
    
    // 傳統函式需要明確傳入參數
    fn add_x_traditional(num: i32, x: i32) -> i32 {
        num + x
    }
    
    // 閉包可以直接捕獲環境中的 x
    let add_x_closure = |num: i32| num + x;
    
    println!("傳統函式:{}", add_x_traditional(2, x));
    println!("閉包:{}", add_x_closure(2));
}

閉包語法

閉包的語法非常靈活,Rust 可以自動推斷大部分型別:

fn main() {
    // 完整語法
    let add_verbose = |x: i32, y: i32| -> i32 { x + y };
    
    // 省略型別標記(編譯器推斷)
    let add_simple = |x, y| x + y;
    
    // 單行表達式
    let double = |x| x * 2;
    
    // 多行程式碼塊
    let complex_calculation = |x| {
        let intermediate = x * 2;
        let result = intermediate + 10;
        println!("計算中:{} -> {}", x, result);
        result
    };
    
    println!("詳細加法:{}", add_verbose(3, 4));
    println!("簡單加法:{}", add_simple(5, 6));
    println!("雙倍:{}", double(7));
    println!("複雜計算:{}", complex_calculation(8));
}

環境捕獲的三種方式

閉包可以透過三種方式捕獲環境變數,對應到 Rust 的三種基本所有權操作:

1. 不可變借用 (FnOnce)

fn main() {
    let list = vec![1, 2, 3];
    println!("定義閉包前:{:?}", list);
    
    // 閉包只是借用 list
    let only_borrows = || println!("從閉包中:{:?}", list);
    
    println!("呼叫閉包前:{:?}", list);
    only_borrows();
    println!("呼叫閉包後:{:?}", list); // list 仍然可用
}

2. 可變借用 (FnMut)

fn main() {
    let mut list = vec![1, 2, 3];
    println!("定義閉包前:{:?}", list);
    
    // 閉包可變地借用 list
    let mut borrows_mutably = || list.push(7);
    
    // println!("這行會編譯錯誤:{:?}", list); // 不能在可變借用期間使用
    borrows_mutably();
    println!("呼叫閉包後:{:?}", list);
}

3. 取得所有權 (Fn)

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("定義閉包前:{:?}", list);
    
    // 使用 move 關鍵字強制閉包取得所有權
    let handle = thread::spawn(move || {
        println!("從執行緒中:{:?}", list);
    });
    
    // println!("這行會編譯錯誤:{:?}", list); // list 已被移動
    handle.join().unwrap();
}

實用的閉包範例

讓我們看一些實際的使用場景:

use std::collections::HashMap;

fn main() {
    // 1. 條件過濾
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let threshold = 5;
    
    let filtered: Vec<i32> = numbers
        .into_iter()
        .filter(|&x| x > threshold)
        .collect();
    
    println!("大於 {} 的數字:{:?}", threshold, filtered);
    
    // 2. 資料轉換
    let words = vec!["hello", "world", "rust", "programming"];
    let prefix = "Lang: ";
    
    let prefixed: Vec<String> = words
        .iter()
        .map(|&word| format!("{}{}", prefix, word.to_uppercase()))
        .collect();
    
    println!("加上前綴:{:?}", prefixed);
    
    // 3. 計算與聚合
    let sales_data = vec![100.0, 250.0, 300.0, 450.0, 200.0];
    let tax_rate = 0.1;
    
    let total_with_tax: f64 = sales_data
        .iter()
        .map(|&amount| amount * (1.0 + tax_rate))
        .sum();
    
    println!("含稅總額:{:.2}", total_with_tax);
    
    // 4. 複雜的業務邏輯
    let users = vec![
        ("Alice", 25, "Engineer"),
        ("Bob", 30, "Designer"),
        ("Charlie", 35, "Manager"),
        ("Diana", 28, "Engineer"),
    ];
    
    let min_age = 27;
    let target_role = "Engineer";
    
    let senior_engineers: Vec<&str> = users
        .iter()
        .filter(|(_, age, role)| *age >= min_age && *role == target_role)
        .map(|(name, _, _)| *name)
        .collect();
    
    println!("資深工程師:{:?}", senior_engineers);
    
    // 5. 高階函式的使用
    let operation = create_multiplier(3);
    let results: Vec<i32> = (1..=5)
        .map(operation)
        .collect();
    
    println!("乘以 3 的結果:{:?}", results);
}

// 回傳閉包的函式
fn create_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor
}

迭代器 (Iterators):高效的資料處理管線

迭代器的基本概念

迭代器是 Rust 中處理資料集合的主要方式,它們提供了一套豐富且高效的 API:

fn main() {
    let vec = vec![1, 2, 3, 4, 5];
    
    // 建立迭代器的三種方式
    
    // 1. iter() - 產生不可變參考的迭代器
    for item in vec.iter() {
        println!("參考:{}", item); // item 的類型是 &i32
    }
    
    // 2. into_iter() - 產生擁有所有權的迭代器
    for item in vec.clone().into_iter() {
        println!("擁有:{}", item); // item 的類型是 i32
    }
    
    // 3. iter_mut() - 產生可變參考的迭代器
    let mut vec_mut = vec![1, 2, 3, 4, 5];
    for item in vec_mut.iter_mut() {
        *item *= 2; // 修改原始值
    }
    println!("修改後:{:?}", vec_mut);
}

迭代器適配器 (Iterator Adaptors)

迭代器適配器讓我們能夠轉換迭代器,它們是惰性的(lazy),只有在被消費時才會執行:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // 鏈式操作:過濾 -> 映射 -> 收集
    let result: Vec<String> = numbers
        .iter()                           // 建立迭代器
        .filter(|&&x| x % 2 == 0)        // 過濾偶數
        .map(|&x| x * x)                 // 計算平方
        .map(|x| format!("{}²", x))      // 轉換為字串
        .collect();                      // 收集結果
    
    println!("偶數的平方:{:?}", result);
    
    // 更複雜的範例
    let words = vec!["hello", "world", "rust", "is", "awesome"];
    
    let processed: Vec<String> = words
        .iter()
        .enumerate()                     // 添加索引
        .filter(|(i, _)| i % 2 == 0)    // 只要偶數索引
        .map(|(i, &word)| format!("{}: {}", i, word.to_uppercase()))
        .collect();
    
    println!("處理後的單字:{:?}", processed);
}

常用的迭代器方法

轉換方法

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // map - 一對一轉換
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("雙倍:{:?}", doubled);
    
    // filter - 條件過濾
    let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
    println!("偶數:{:?}", evens);
    
    // enumerate - 添加索引
    let with_index: Vec<(usize, &i32)> = numbers.iter().enumerate().collect();
    println!("帶索引:{:?}", with_index);
    
    // zip - 組合兩個迭代器
    let letters = vec!['a', 'b', 'c'];
    let pairs: Vec<(&i32, &char)> = numbers.iter().zip(letters.iter()).collect();
    println!("配對:{:?}", pairs);
    
    // take - 取前 n 個元素
    let first_three: Vec<&i32> = numbers.iter().take(3).collect();
    println!("前三個:{:?}", first_three);
    
    // skip - 跳過前 n 個元素
    let skip_two: Vec<&i32> = numbers.iter().skip(2).collect();
    println!("跳過前兩個:{:?}", skip_two);
}

消費方法

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // collect - 收集成集合
    let collected: Vec<&i32> = numbers.iter().collect();
    println!("收集:{:?}", collected);
    
    // fold - 摺疊(類似 reduce)
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("總和:{}", sum);
    
    // reduce - 簡化版的 fold
    let product = numbers.iter().reduce(|acc, &x| acc * x);
    println!("乘積:{:?}", product);
    
    // for_each - 對每個元素執行動作
    print!("迭代:");
    numbers.iter().for_each(|x| print!("{} ", x));
    println!();
    
    // find - 找到第一個符合條件的元素
    let found = numbers.iter().find(|&&x| x > 3);
    println!("第一個大於 3 的:{:?}", found);
    
    // any / all - 檢查條件
    let has_even = numbers.iter().any(|&&x| x % 2 == 0);
    let all_positive = numbers.iter().all(|&&x| x > 0);
    println!("有偶數:{},全為正數:{}", has_even, all_positive);
    
    // count - 計算數量
    let count = numbers.iter().filter(|&&x| x > 2).count();
    println!("大於 2 的數量:{}", count);
}

效能優勢:零成本抽象

Rust 的迭代器是「零成本抽象」的典型例子,它們編譯後的效能與手寫的迴圈相當:

fn main() {
    let large_vec: Vec<i32> = (0..1_000_000).collect();
    
    // 函數式風格
    let sum_functional: i32 = large_vec
        .iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .sum();
    
    // 傳統迴圈風格
    let mut sum_imperative = 0;
    for &item in &large_vec {
        if item % 2 == 0 {
            sum_imperative += item * item;
        }
    }
    
    println!("函數式結果:{}", sum_functional);
    println!("迴圈結果:{}", sum_imperative);
    assert_eq!(sum_functional, sum_imperative);
    
    // 在 release 模式下,這兩種方式的效能幾乎相同!
}

進階技巧:自訂迭代器

我們也可以為自己的型別實現 Iterator trait:

struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Counter {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;

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

fn main() {
    let counter = Counter::new(5);
    
    // 現在可以使用所有迭代器方法
    let doubled: Vec<usize> = counter
        .map(|x| x * 2)
        .filter(|&x| x > 2)
        .collect();
    
    println!("自訂迭代器結果:{:?}", doubled);
    
    // 或者直接在 for 迴圈中使用
    for num in Counter::new(3) {
        println!("計數:{}", num);
    }
}

效能最佳化技巧

1. 適當使用 collect()

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // ❌ 不必要的中間收集
    let intermediate: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    let result: i32 = intermediate.iter().sum();
    
    // ✅ 直接鏈式操作
    let result: i32 = numbers.iter().map(|x| x * 2).sum();
    
    println!("結果:{}", result);
}

2. 使用 Iterator::fold 而非多次遍歷

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // ❌ 多次遍歷
    let sum: i32 = numbers.iter().sum();
    let count = numbers.len();
    let max = numbers.iter().max();
    
    // ✅ 單次遍歷
    let (sum, count, max) = numbers.iter().fold(
        (0, 0, i32::MIN),
        |(sum, count, max), &x| (sum + x, count + 1, max.max(x))
    );
    
    println!("總和:{},數量:{},最大值:{}", sum, count, max);
}

3. 選擇合適的迭代器方法

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // 查找第一個符合條件的元素
    // ✅ 使用 find(短路求值)
    let first_even = numbers.iter().find(|&&x| x % 2 == 0);
    
    // ❌ 使用 filter + first(處理全部元素)
    let first_even_bad = numbers.iter().filter(|&&x| x % 2 == 0).next();
    
    // 檢查是否存在符合條件的元素
    // ✅ 使用 any(短路求值)
    let has_even = numbers.iter().any(|&x| x % 2 == 0);
    
    // ❌ 使用 filter + count(處理全部元素)
    let has_even_bad = numbers.iter().filter(|&&x| x % 2 == 0).count() > 0;
    
    println!("第一個偶數:{:?},有偶數:{}", first_even, has_even);
}

與其他語言的比較

JavaScript 風格 vs Rust 風格

// JavaScript
const numbers = [1, 2, 3, 4, 5];
const result = numbers
    .filter(x => x % 2 === 0)
    .map(x => x * x)
    .reduce((sum, x) => sum + x, 0);
// Rust
let numbers = vec![1, 2, 3, 4, 5];
let result: i32 = numbers
    .iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * x)
    .sum(); // sum() 是 reduce 的特化版本

Python 風格 vs Rust 風格

# Python
numbers = [1, 2, 3, 4, 5]
result = sum(x * x for x in numbers if x % 2 == 0)
// Rust
let numbers = vec![1, 2, 3, 4, 5];
let result: i32 = numbers
    .iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * x)
    .sum();

函數式程式設計的最佳實務

1. 優先使用迭代器而非手寫迴圈

// ✅ 好:使用迭代器
fn sum_squares(numbers: &[i32]) -> i32 {
    numbers.iter().map(|&x| x * x).sum()
}

// ❌ 較差:手寫迴圈
fn sum_squares_manual(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &number in numbers {
        sum += number * number;
    }
    sum
}

2. 使用函數組合而非巢狀邏輯

// ✅ 好:函數組合
fn process_users(users: &[User]) -> Vec<String> {
    users
        .iter()
        .filter(|user| user.is_active())
        .filter(|user| user.age >= 18)
        .map(|user| user.email.to_uppercase())
        .collect()
}

// ❌ 較差:巢狀邏輯
fn process_users_nested(users: &[User]) -> Vec<String> {
    let mut result = Vec::new();
    for user in users {
        if user.is_active() {
            if user.age >= 18 {
                result.push(user.email.to_uppercase());
            }
        }
    }
    result
}

3. 善用部分應用和閉包組合

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // 建立可重用的謂詞
    let is_even = |x: &i32| *x % 2 == 0;
    let is_positive = |x: &i32| *x > 0;
    let greater_than = |threshold| move |x: &i32| *x > threshold;
    
    // 組合使用
    let result: Vec<&i32> = numbers
        .iter()
        .filter(is_even)
        .filter(is_positive)
        .filter(greater_than(5))
        .collect();
    
    println!("結果:{:?}", result);
}

今天的收穫

今天我們深入探討了 Rust 中函數式程式設計的兩大支柱:

閉包 (Closures)

  • 環境捕獲:三種捕獲方式(FnOnce, FnMut, Fn)
  • 語法靈活性:從完整標記到型別推斷
  • 實用場景:事件處理、資料轉換、高階函式

迭代器 (Iterators)

  • 零成本抽象:編譯後效能媲美手寫迴圈
  • 豐富的 API:map, filter, fold, collect 等方法
  • 鏈式操作:建立資料處理管線

實用技巧

  • 適當使用 collect() 避免不必要的中間集合
  • 選擇正確的迭代器方法提升效能
  • 使用函數組合提高程式碼可讀性

設計原則

  • 優先使用迭代器而非手寫迴圈
  • 善用閉包實現靈活的抽象
  • 組合小函式來解決複雜問題

為什麼重要?

  • 效能優勢:零成本抽象,編譯時優化
  • 可讀性提升:程式碼意圖更明確
  • 安全性保證:型別系統防止常見錯誤
  • 並發友善:函數式風格天然適合並行處理

閉包和迭代器是現代 Rust 程式設計的重要工具,它們讓我們能夠寫出既高效又優雅的程式碼。掌握這些概念將大大提升你的 Rust 程式設計水準。

今天的小挑戰

為了鞏固今天的學習,嘗試實作一個日誌分析器

功能需求

  1. 日誌解析:解析 Apache/Nginx 風格的日誌格式
  2. 統計功能:計算狀態碼分布、最熱門的頁面、錯誤率
  3. 時間分析:按小時/天統計流量
  4. IP 分析:找出最活躍的 IP 地址
  5. 過濾功能:支援按時間範圍、狀態碼、URL 模式過濾

技術要求

  • 使用閉包實現靈活的過濾邏輯
  • 使用迭代器進行高效的資料處理
  • 實現鏈式 API 支援複雜查詢
  • 處理大量日誌資料(考慮記憶體效率)

技術提示

struct LogEntry {
    ip: String,
    timestamp: String,
    method: String,
    url: String,
    status: u16,
    size: u64,
    user_agent: String,
}

struct LogAnalyzer {
    entries: Vec<LogEntry>,
}

impl LogAnalyzer {
    fn filter_by<F>(&self, predicate: F) -> impl Iterator<Item = &LogEntry>
    where
        F: Fn(&LogEntry) -> bool,
    {
        self.entries.iter().filter(predicate)
    }
    
    fn status_distribution(&self) -> HashMap<u16, usize> {
        // 使用迭代器統計狀態碼分布
    }
    
    fn top_pages(&self, limit: usize) -> Vec<(String, usize)> {
        // 找出最熱門的頁面
    }
    
    fn hourly_traffic(&self) -> Vec<(u8, usize)> {
        // 按小時統計流量
    }
}

這個挑戰將讓你綜合運用閉包和迭代器來處理實際的資料分析問題。重點是要善用函數式程式設計的思維,寫出既高效又優雅的程式碼。

明天我們將學習 併發 (Concurrency),探討 Rust 如何讓我們安全地編寫多執行緒程式。這是 Rust 的另一個強項,結合今天學到的函數式技巧,將讓我們能夠建立高效能的並行資料處理系統!

如果在實作過程中遇到任何問題,歡迎在留言區討論。函數式程式設計需要一些時間來適應,但一旦掌握,你會發現它讓程式碼變得更加優雅和強大!

我們明天見!


上一篇
Day 14: 測試 (Testing):建立可靠的測試體系
下一篇
Day 16: 併發 (Concurrency):不再害怕多執行緒
系列文
大家一起跟Rust當好朋友吧!18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言