閉包與迭代器
閉包(closure)是可以捕獲周圍變數的匿名函式;迭代器(iterator)則是處理一連串資料的抽象。兩者搭配,可以用 map、filter 這類串接的寫法取代手寫迴圈,而且效能相同。這是 Rust 中很有代表性的「高階抽象、零成本」範例。
閉包
語法是 |參數| 運算式,參數與回傳型別通常可以省略,由編譯器推斷:
fn main() {
let add_one = |x: i32| x + 1;
let square = |x| x * x;
let greet = |name: &str| {
let msg = format!("哈囉,{name}!");
msg
};
println!("{} {} {}", add_one(5), square(4), greet("Rust"));
}
捕獲環境
閉包能直接使用外部的變數,這是它與一般函式最大的差別:
fn main() {
let threshold = 10;
let is_big = |n: i32| n > threshold; // 借用 threshold
println!("{} {}", is_big(5), is_big(20));
}
閉包會依需求自動選擇最省的捕獲方式:唯讀借用、可修改借用,或取走所有權。若要強制取走所有權,在前面加上 move,常用於把閉包交給另一個執行緒:
fn main() {
let name = String::from("Rust");
let own = move || println!("擁有 {name}");
own();
// println!("{name}"); // 編譯錯誤:name 已經被移進閉包
}
三種閉包 trait
編譯器依閉包如何使用捕獲的值,自動為它實作以下 trait,函式參數要接收閉包時會用到:
| Trait | 說明 |
|---|---|
FnOnce |
只能呼叫一次(會取走捕獲的值) |
FnMut |
可以多次呼叫,且會修改捕獲的值 |
Fn |
可以多次呼叫,只讀取捕獲的值 |
fn apply<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
f(value)
}
fn main() {
let factor = 3;
println!("{}", apply(|x| x * factor, 7));
}
迭代器
迭代器是實作了 Iterator trait 的型別,核心是 next() 方法。for 迴圈背後就是不斷呼叫 next()。取得迭代器的三種常見方式:
| 方法 | 產生的元素 | 說明 |
|---|---|---|
.iter() |
&T |
唯讀借用,集合之後仍可使用 |
.iter_mut() |
&mut T |
可修改借用 |
.into_iter() |
T |
取走所有權,集合之後不能使用 |
迭代器是惰性(lazy)的:串接 map、filter 這類轉接器(adaptor)本身不會做任何事,要等到呼叫 collect、sum、for 這類消費者(consumer)才會真正執行。
常用操作
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6];
// filter 留下偶數,map 求平方,collect 收集成 Vec
let result: Vec<i32> = nums
.iter()
.filter(|&&n| n % 2 == 0)
.map(|n| n * n)
.collect();
println!("{:?}", result);
// 直接算總和與計數
let sum: i32 = nums.iter().sum();
let count = nums.iter().filter(|&&n| n > 3).count();
println!("{sum} {count}");
// 加上索引、兩個序列配對
for (i, n) in nums.iter().enumerate().take(2) {
println!("{i}: {n}");
}
let names = ["甲", "乙", "丙"];
for (name, n) in names.iter().zip(nums.iter()) {
println!("{name} = {n}");
}
// 尋找、判斷
println!("{:?}", nums.iter().find(|&&n| n > 4));
println!("{}", nums.iter().any(|&n| n == 3));
println!("{}", nums.iter().all(|&n| n > 0));
}
collect 可以收集成不同型別
collect 依你指定的型別決定輸出,例如收集成 HashMap 或 String:
use std::collections::HashMap;
fn main() {
let words = ["apple", "fig", "banana"];
let lengths: HashMap<&str, usize> = words.iter().map(|w| (*w, w.len())).collect();
println!("{}", lengths["fig"]);
let shout: String = words.iter().map(|w| w.to_uppercase()).collect::<Vec<_>>().join("-");
println!("{shout}");
}
自訂迭代器
只要實作 next(),你的型別就能使用所有迭代器方法:
struct Countdown(u32);
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.0 == 0 {
None
} else {
self.0 -= 1;
Some(self.0 + 1)
}
}
}
fn main() {
let items: Vec<u32> = Countdown(3).collect();
println!("{:?}", items);
let total: u32 = Countdown(4).map(|n| n * 10).sum();
println!("{total}");
}
迭代器會比手寫迴圈慢嗎?
不會。迭代器鏈在編譯時會被展開、內聯並最佳化,效能與手寫的 for 迴圈相當,這就是所謂的「零成本抽象」。可讀性通常更好,也不會出現索引越界。
推薦影音
閉包
簡述:Let's Get Rusty 依照官方書籍第 13 章製作,說明閉包的語法、捕獲方式與三種閉包 trait。
迭代器
簡述:同一系列的迭代器單元,介紹 Iterator trait、轉接器與消費者,對應本頁「迭代器」一節。
想更深入的話,可以到 影音教學總覽 看 Jon Gjengset 的《Crust of Rust: Iterators》。
接下來前往 進階主題。