Rust 的閉包(closures)是可以保存在一個變量中或作為參數傳遞給其他函數的匿名函數。可以在一個地方創建閉包,然后在不同的上下文中執行閉包運算。不同于函數,閉包允許捕獲被定義時所在作用域中的值。
迭代器(iterator)負責遍歷序列中的每一項和決定序列何時結束的邏輯。當使用迭代器時,我們無需重新實現這些邏輯。
一、閉包
閉包,一個可以儲存在變量里的類似函數的結構。
1.1 閉包會捕獲其環境
在 Rust 中,閉包是一種匿名函數,它們可以捕獲外部環境中的變量。閉包的捕獲行為取決于變量的類型和閉包如何使用這些變量。
通過值捕獲(By Value)
當閉包通過值捕獲一個變量時,它獲取了該變量的所有權。這意味著在閉包被創建之后,原始變量不能再被使用。
fn main() {let text = "Hello".to_string();// 使用 move 來顯式地表示閉包將獲取 text 的所有權let closure = move || println!("{}", text);// 這里 text 不能被使用,因為其所有權已經被閉包獲取// println!("{}", text); // 這將導致編譯錯誤closure(); // 打印 "Hello"
}
通過引用捕獲(By Reference)
當閉包通過引用捕獲一個變量時,它借用了該變量。這意味著原始變量仍然可用,但閉包只能借用它,不能獲取所有權。
fn main() {let text = "Hello";// 閉包通過引用捕獲 textlet closure = || println!("{}", text);// text 仍然可用,因為它沒有被移動println!("{}", text); // 打印 "Hello"closure(); // 再次打印 "Hello"
}
可變捕獲(Mutable Capture)
閉包可以捕獲一個可變引用,允許它修改原始變量的值。
fn main() {let mut count = 0;// 閉包通過可變引用捕獲 countlet mut closure = || {count += 1; // 修改 count 的值println!("Count: {}", count);};closure(); // 打印 "Count: 1"closure(); // 打印 "Count: 2"// count 的值現在是 2println!("Final count: {}", count);
}
如果移除閉包的 mut
修飾,編譯不通過。
fn main() {let mut count = 0;// 閉包通過可變引用捕獲 countlet closure = || {count += 1; // 修改 count 的值println!("Count: {}", count);};closure(); // 打印 "Count: 1"closure(); // 打印 "Count: 2"// count 的值現在是 2println!("Final count: {}", count);
}
報錯信息提示:由于借用了可變的 count
,調用 closure
需要可變的綁定。
Compiling playground v0.0.1 (/playground)
error[E0596]: cannot borrow `closure` as mutable, as it is not declared as mutable--> src/main.rs:4:9|
4 | let closure = || {| ^^^^^^^ not mutable
5 | count += 1; // 修改 count 的值| ----- calling `closure` requires mutable binding due to mutable borrow of `count`
...
8 | closure(); // 打印 "Count: 1"| ------- cannot borrow as mutable
9 | closure(); // 打印 "Count: 2"| ------- cannot borrow as mutable|
help: consider changing this to be mutable|
4 | let mut closure = || {| +++For more information about this error, try `rustc --explain E0596`.
error: could not compile `playground` (bin "playground") due to 1 previous error
閉包作為參數
閉包可以作為參數傳遞給其他函數,捕獲環境中的變量。
fn main() {// 創建一個整數變量let number = 10;// 創建一個閉包,它接受一個 i32 類型的參數并返回其平方// 這里使用 || 表示這是一個閉包let square = || number * number;// 定義一個函數,它接受一個閉包作為參數并調用它// 閉包作為參數需要指定其類型,這里使用 || -> i32 表示閉包沒有參數并返回 i32 類型的值fn call_closure<F>(f: F)whereF: Fn() -> i32, // 使用 trait bound 指定閉包的簽名{// 調用閉包并打印結果let result = f();println!("The result is: {}", result);}// 調用 `call_closure` 函數,并將閉包 `square` 作為參數傳遞// 由于閉包 `square` 沒有參數,我們可以直接傳遞call_closure(square);
}
在這個示例中:
- 我們定義了一個閉包
square
,它通過引用捕獲了main
函數中的number
變量,并計算其平方。 - 我們定義了一個名為
call_closure
的函數,它接受一個符合Fn() -> i32
trait bound 的閉包作為參數,這意味著閉包沒有參數并返回一個i32
類型的值。 - 我們調用
call_closure
函數,并將square
閉包作為參數傳遞。由于square
閉包沒有參數,它可以直接作為參數傳遞給call_closure
。 call_closure
函數調用閉包并打印出結果。
1.2 閉包類型推斷和注解
函數與閉包還有更多區別。閉包并不總是要求像 fn
函數那樣在參數和返回值上注明類型。函數中需要類型注解是因為它們是暴露給用戶的顯式接口的一部分。嚴格定義這些接口對保證所有人都對函數使用和返回值的類型理解一致是很重要的。與此相比,閉包并不用于這樣暴露在外的接口:它們儲存在變量中并被使用,不用命名它們或暴露給庫的用戶調用。
閉包通常很短,并只關聯于小范圍的上下文而非任意情境。在這些有限制的上下文中,編譯器能可靠地推斷參數和返回值的類型,類似于它是如何能夠推斷大部分變量的類型一樣(同時也有編譯器需要閉包類型注解的罕見情況)。
類似于變量,如果我們希望增加明確性和清晰度也可以添加類型標注,壞處是使代碼變得更啰嗦(相對于嚴格必要的代碼)。
fn main() {let a = 100;let add_one = |x: i32| -> i32 { x + 1 };let b = add_one(a);println!("{}", b);
}
有了類型注解閉包的語法就更類似函數了。如下是一個對其參數加一的函數的定義與擁有相同行為閉包語法的縱向對比。這里增加了一些空格來對齊相應部分。這展示了除了使用豎線以及一些可選語法外,閉包語法與函數語法有多么地相似:
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 ;
所以上例可以簡化為:
fn main() {let a = 100;let add_one = |x| x + 1;let b = add_one(a);println!("{}", b);
}
編譯器會為閉包定義中的每個參數和返回值推斷一個具體類型。
再來看一個示例:
fn main() {let a = 100i32;let a1 = 100f32;let closure = |x| x;let b = closure(a);let b1 = closure(a1);println!("{}", b); println!("{}", b1);
}
注意這個閉包定義沒有增加任何類型注解,所以我們可以用任意類型來調用這個閉包。但是如果嘗試調用閉包兩次,第一次使用 i32,第二次使用 f32 就會報錯:預期的,因為之前使用“i32”類型的參數調用了閉包。
Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types--> src/main.rs:6:22|
6 | let b1 = closure(a1);| ------- ^^ expected `i32`, found `f32`| || arguments to this function are incorrect|
note: expected because the closure was earlier called with an argument of type `i32`--> src/main.rs:5:21|
5 | let b = closure(a);| ------- ^ expected because this argument is of type `i32`| || in this closure call
note: closure parameter defined here--> src/main.rs:4:20|
4 | let closure = |x| x;| ^For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to 1 previous error
1.3 將被捕獲的值移出閉包和 Fn trait
一旦閉包捕獲了定義它的環境中一個值的引用或者所有權(也就影響了什么會被移進閉包,如有),閉包體中的代碼定義了稍后在閉包計算時對引用或值如何操作(也就影響了什么會被移出閉包,如有)。閉包體可以做以下任何事:將一個捕獲的值移出閉包,修改捕獲的值,既不移動也不修改值,或者一開始就不從環境中捕獲值。
閉包捕獲和處理環境中的值的方式影響閉包實現的 trait。Trait 是函數和結構體指定它們能用的閉包的類型的方式。取決于閉包體如何處理值,閉包自動、漸進地實現一個、兩個或三個 Fn
trait。
FnOnce
適用于能被調用一次的閉包,所有閉包都至少實現了這個 trait,因為所有閉包都能被調用。一個會將捕獲的值移出閉包體的閉包只實現FnOnce
trait,這是因為它只能被調用一次。FnMut
適用于不會將捕獲的值移出閉包體的閉包,但它可能會修改被捕獲的值。這類閉包可以被調用多次。Fn
適用于既不將被捕獲的值移出閉包體也不修改被捕獲的值的閉包,當然也包括不從環境中捕獲值的閉包。這類閉包可以被調用多次而不改變它們的環境,這在會多次并發調用閉包的場景中十分重要。
下面是一個示例,展示如何在函數中使用 FnOnce
作為泛型約束,并且確保閉包只被調用一次:
fn call_once<F, T>(f: F) -> T
whereF: FnOnce() -> T, // 約束 F 為 FnOnce trait,意味著它接受一個空參數并返回 T 類型
{f() // 調用閉包并返回結果
}fn main() {// 創建一個閉包,它捕獲了 `value` 的所有權let value = 42;let consume = move || {let result = value; // 移動 `value`println!("The value is: {}", result);result // 返回結果};// 調用 `call_once` 函數,傳入閉包let result = call_once(consume);println!("Result of the closure: {}", result);// 嘗試再次使用 `consume` 將會導致編譯錯誤,因為它已經消耗了 `value`// call_once(consume);
}
運行結果
The value is: 42
Result of the closure: 42
在這個示例中,我們定義了一個泛型函數 call_once
,它接受一個類型為 F
的參數 f
,其中 F
必須實現 FnOnce
trait。這意味著 f
是一個閉包,它接受空參數并返回類型 T
的結果。
在 main
函數中,我們創建了一個閉包 consume
,它捕獲了 value
的所有權。然后,我們調用 call_once
函數,傳入 consume
閉包。call_once
函數調用閉包并返回其結果。由于 consume
閉包已經消耗了 value
,嘗試再次調用 call_once
傳入相同的閉包將會導致編譯錯誤。
下面是一個使用 FnMut
trait 的示例。
fn apply_mut<F, T>(func: &mut F, num: i32) -> T
whereF: FnMut(i32) -> T, // F 是一個可變閉包,接受一個 i32 類型的參數并返回類型為 T 的結果
{func(num) // 調用閉包并返回結果
}fn main() {let mut count = 0;// 創建一個閉包,它接受一個 i32 類型的參數并將其加到 count 上let mut increment = |num: i32| -> i32 {count += num;count};// 使用 apply_mut 函數和 increment 閉包的引用let result: i32 = apply_mut(&mut increment, 5);println!("Result after applying increment: {}", result);// 再次使用 apply_mut 函數和 increment 閉包的引用let result: i32 = apply_mut(&mut increment, 10);println!("Result after applying increment again: {}", result);
}
運行結果
Result after applying increment: 5
Result after applying increment again: 15
在這個例子中,apply_mut
函數接受一個 F
類型的可變引用 &mut F
作為參數(這樣,increment
閉包就不會被移動,可以被多次使用)和一個 i32
類型的參數 num
。where
子句指定了 F
必須是一個實現了 FnMut
的閉包,它接受一個 i32
類型的參數并返回一個類型為 T
的結果。main
函數中創建了一個可變閉包 increment
,它修改了一個捕獲的變量 count
,然后使用 apply_mut
函數來調用這個閉包。每次調用 apply_mut
都會使用 increment
閉包,并且 increment
閉包都會修改 count
的值。
在 Rust 中,Fn
trait 表示閉包不會獲取它捕獲變量的所有權,也不會改變這些變量。下面的例子展示了如何定義一個接受 Fn
閉包作為參數的函數,并在并發場景中使用它。
use std::thread;// 定義一個函數,它接受一個實現了 Fn(i32) -> i32 的閉包,并調用它
fn call_once<F, T>(func: F) -> T
whereF: Fn(i32) -> T, // 指定 F 是一個接受 i32 并返回 T 的 Fn 閉包
{let result = func(42); // 調用閉包,傳入一個 i32 類型的值result // 返回閉包的執行結果
}fn main() {// 定義一個簡單的 Fn 閉包,它接受一個 i32 類型的參數并返回兩倍的該值let double = |x: i32| -> i32 {x * 2};// 創建多個線程,每個線程都使用相同的閉包let handles: Vec<_> = (0..5).map(|i| {let func = double; // 閉包可以被復制,因為它是 Fn 類型的thread::spawn(move || {let result = call_once(func); // 調用 call_once 函數,并傳入閉包println!("Thread {} result: {}", i, result); // 打印結果})}).collect();// 等待所有線程完成for handle in handles {handle.join().unwrap();}
}
不是每次運行都是這個順序,具體哪個線程先運行,要看系統的本次調度。
運行結果
Thread 1 result: 84
Thread 2 result: 84
Thread 0 result: 84
Thread 4 result: 84
Thread 3 result: 84
在這個例子中:
- 我們定義了一個
call_once
函數,它接受一個泛型參數F
,并且F
必須滿足Fn(i32) -> T
的 trait bounds。這意味著F
是一個閉包,它接受一個i32
類型的參數并返回一個類型為T
的結果。 - 在
main
函數中,我們定義了一個簡單的閉包double
,它接受一個i32
類型的參數x
并返回x * 2
的結果。 - 我們使用
map
創建了 5 個線程,每個線程都復制了double
閉包,并在新線程中調用call_once
函數,將閉包作為參數傳遞給call_once
。 - 在每個線程中,
call_once
函數被調用,執行閉包,并打印出結果。 - 最后,我們使用
join
方法等待所有線程完成。
二、迭代器
迭代器允許你對一個序列的項進行某些處理。接下來主要介紹使用迭代器處理元素序列和循環 VS 迭代器性能比較。在 Rust 中,迭代器是惰性的(lazy),這意味著在調用消費迭代器的方法之前不會執行任何操作。
2.1 Iterator trait 和 next 方法
迭代器都實現了一個叫做 Iterator
的定義于標準庫的 trait。這個 trait 的定義看起來像這樣:
pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;// 此處省略了方法的默認實現
}
type Item
和 Self::Item
,它們定義了 trait 的關聯類型(associated type)。不過現在只需知道這段代碼表明實現 Iterator
trait 要求同時定義一個 Item
類型,這個 Item
類型被用作 next
方法的返回值類型。換句話說,Item
類型將是迭代器返回元素的類型。
next
是 Iterator
實現者被要求定義的唯一方法。next
一次返回迭代器中的一個項,封裝在 Some
中,當迭代器結束時,它返回 None
。
2.2 消費迭代器的方法
Iterator
trait 有一系列不同的由標準庫提供默認實現的方法;你可以在 Iterator
trait 的標準庫 API 文檔中找到所有這些方法。一些方法在其定義中調用了 next
方法,這也就是為什么在實現 Iterator
trait 時要求實現 next
方法的原因。
這些調用 next
方法的方法被稱為消費適配器(consuming adaptors),因為調用它們會消耗迭代器。
fn main() {let numbers = vec![1, 2, 3, 4, 5];// 使用into_iter()將Vec轉換為消費迭代器let numbers_iter = numbers.iter();let mut sum: i32 = numbers_iter// 使用sum適配器計算迭代器中所有元素的總和.sum();// 打印總和println!("The sum is: {}", sum);
}
如果再去使用 numbers_iter
就會報錯。 sum
方法獲取迭代器的所有權并反復調用 next
來遍歷迭代器,因而會消費迭代器。當其遍歷每一個項時,它將每一個項加總到一個總和并在迭代完成時返回總和。調用 sum
之后不再允許使用 numbers_iter
,因為調用 sum
時它會獲取迭代器的所有權。
2.3 產生其他迭代器的方法
Iterator
trait 中定義了另一類方法,被稱為迭代器適配器(iterator adaptors),它們允許我們將當前迭代器變為不同類型的迭代器。可以鏈式調用多個迭代器適配器。不過因為所有的迭代器都是惰性的,必須調用一個消費適配器方法以便獲取迭代器適配器調用的結果。
fn main() {let v1: Vec<i32> = vec![1, 2, 3];let v2: Vec<_> = v1.iter().map(|x| x * x).collect();assert_eq!(v2, vec![1, 4, 9]);
}
collect 方法消費迭代器并將結果收集到一個數據結構中。因為 map
獲取一個閉包,可以指定任何希望在遍歷的每個元素上執行的操作。
2.4 使用捕獲其環境的閉包
很多迭代器適配器接受閉包作為參數,而通常指定為迭代器適配器參數的閉包會是捕獲其環境的閉包。
fn main() {let numbers = vec![1, 2, 3, 4, 5];// 使用into_iter()將Vec轉換為消費迭代器let filtered_and_squared: Vec<i32> = numbers.into_iter()// 使用filter適配器過濾元素,接受一個閉包作為參數// 閉包捕獲其環境,這里指numbers的元素.filter(|&x| x % 2 == 0) // 保留偶數// 使用map適配器對過濾后的元素進行變換,也接受一個閉包.map(|x| x * x) // 對每個元素進行平方// 使用collect適配器將結果收集到一個新的Vec中.collect();// 打印過濾和平方后的結果println!("The filtered and squared numbers are: {:?}", filtered_and_squared);
}
運行結果
The filtered and squared numbers are: [4, 16]
在這個例子中,filter
和 map
都是迭代器適配器,它們接受閉包作為參數。這些閉包可以捕獲其環境,即迭代器中的元素。在 filter
適配器中,閉包 |&x| x % 2 == 0
用于檢查元素是否為偶數,如果是,則元素會被保留。在 map
適配器中,閉包 |x| x * x
用于對每個元素進行平方操作。由于這些適配器都是消費性的,它們會消耗原始的迭代器,因此不能再次使用。最后,collect
適配器將處理后的元素收集到一個新的 Vec
中。
2.5 循環 VS 迭代器性能比較
迭代器,作為一個高級的抽象,被編譯成了與手寫的底層代碼大體一致性能的代碼。迭代器是 Rust 的零成本抽象(zero-cost abstractions)之一,它意味著抽象并不會引入運行時開銷。
fn main() {let numbers1 = (0..1000000).collect::<Vec<i64>>();let numbers2 = (0..1000000).collect::<Vec<i64>>();let mut sum1 = 0i64;let mut sum2 = 0i64;// 測量for循環的性能let start = std::time::Instant::now();for val in numbers1 {sum1 += val;}let loop_duration = start.elapsed();// 測量迭代器的性能let start = std::time::Instant::now();for val in numbers2.iter() {sum2 += val;}let iterator_duration = start.elapsed();println!("Iterator took: {:?}", iterator_duration);println!("For loop took: {:?}", loop_duration);
}
運行結果
Iterator took: 12.796012ms
For loop took: 11.559512msIterator took: 12.817732ms
For loop took: 11.687655msIterator took: 12.75484ms
For loop took: 11.89468msIterator took: 12.812022ms
For loop took: 11.785106msIterator took: 12.78293ms
For loop took: 11.528941ms
從這個例子可以看出迭代器還是稍微慢一些。
參考鏈接
- Rust 官方網站:https://www.rust-lang.org/zh-CN
- Rust 官方文檔:https://doc.rust-lang.org/
- Rust Play:https://play.rust-lang.org/
- 《Rust 程序設計語言》