Rust 閉包提供一種簡潔的匿名函式機制,能捕捉周圍環境變數,方便處理迭代器、執行緒和預設值計算等場景。然而,在多執行緒環境下,閉包的變數捕捉方式至關重要。Rust 沒有垃圾回收機制,而是透過借用和生命週期規則確保安全性。當閉包借用變數時,其生命週期受限於被借用變數的生命週期。若閉包的生命週期可能超過被借用變數,則需使用 move 關鍵字將變數所有權轉移至閉包,避免懸空指標等問題。理解 Fn、FnMut 和 FnOnce 三種閉包型別,以及它們與變數捕捉方式的關聯,是確保執行緒安全的關鍵。Fn 閉包不可變借用變數,FnMut 可變借用,而 FnOnce 則會消耗變數,因此只能呼叫一次。這三種型別閉包各有其適用場景,例如,FnOnce 適用於需要修改或消耗變數的場合,而 Fn 則適用於只需讀取變數的場合。
閉包(Closures)
「拯救環境!今天就建立一個閉包吧!」 —— Cormac Flanagan
對一組整數向量進行排序非常簡單:
integers.sort();
然而,令人遺憾的是,當我們需要對某些資料進行排序時,它們很少是整數向量。我們通常擁有某種記錄,而內建的排序方法通常無法直接使用:
struct City {
name: String,
population: i64,
country: String,
// ...
}
fn sort_cities(cities: &mut Vec<City>) {
cities.sort(); // 錯誤:你希望如何排序?
}
Rust 抱怨說 City 沒有實作 std::cmp::Ord。我們需要指定排序順序,如下所示:
/// 輔助函式,用於按人口數量降序排序城市。
fn city_population_descending(city: &City) -> i64 {
-city.population
}
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(city_population_descending); // ok
}
輔助函式 city_population_descending 接受一個 City 記錄並提取鍵值,即我們希望據以排序資料的欄位。(它傳回一個負數,因為 sort 方法按升序排列數字,而我們希望降序排列:人口最多的城市排在最前面。)sort_by_key 方法接受這個鍵值函式作為引數。
這樣做可以正常運作,但寫成閉包(Closure)會更簡潔,閉包是一種匿名函式表示式:
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(|city| -city.population);
}
這裡的閉包是 |city| -city.population。它接受一個引數 city 並傳回 -city.population。Rust 根據閉包的使用方式推斷引數型別和傳回型別。
標準函式庫中其他接受閉包的範例如下:
- 用於處理順序資料的迭代器方法,如
map和filter。我們將在第 15 章介紹這些方法。 - 執行緒 API,如
thread::spawn,它啟動一個新的系統執行緒。平行處理是將工作移到其他執行緒,而閉包很方便地表示工作單元。我們將在第 19 章介紹這些功能。 - 一些方法需要計算預設值,如
HashMap條目的or_insert_with方法。當預設值計算開銷很大時,會使用此方法。預設值以閉包形式傳遞,只有在需要建立新條目時才會呼叫。
捕捉變數(Capturing Variables)
閉包可以使用所屬封閉函式的資料。例如:
/// 按多種不同的統計資料排序。
fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat));
}
這裡的閉包使用 stat,它由封閉函式 sort_by_statistic 所擁有。我們說這個閉包「捕捉」了 stat。這是閉包的經典特性之一,因此 Rust 自然支援它;但在 Rust 中,這個特性帶有附加條件。
在大多數具有閉包的語言中,垃圾回收扮演著重要的角色。例如,考慮以下 JavaScript 程式碼:
// 啟動一個動畫,重新排列城市表格中的列。
function startSortingAnimation(cities, stat) {
// 輔助函式,用於排序表格。
// 注意這個函式參照了 stat。
function keyfn(city) {
return city.get_statistic(stat);
}
if (pendingSort)
pendingSort.cancel();
// 現在啟動動畫,將 keyfn 傳遞給它。
// 排序演算法稍後將呼叫 keyfn。
pendingSort = new SortingAnimation(cities, keyfn);
}
閉包 keyfn 被儲存在新的 SortingAnimation 物件中。它應該在 startSortingAnimation 傳回後被呼叫。通常,當一個函式傳回時,它的所有變數和引數都會超出範圍並被丟棄。但在這裡,JavaScript 引擎必須以某種方式保留 stat,因為閉包使用了它。大多數 JavaScript 引擎透過在堆積中分配 stat 並讓垃圾回收器稍後回收它來實作這一點。
Rust 沒有垃圾回收。這將如何運作?為了回答這個問題,我們將檢視兩個例子。
借用閉包(Closures That Borrow)
首先,讓我們重複本文的開頭例子:
fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat));
}
在這種情況下,當 Rust 建立閉包時,它會自動借用對 stat 的參照。很合理:閉包參照了 stat,因此它必須有對它的參照。其餘的很簡單。閉包受我們在第 5 章中描述的借用和生命週期規則的約束。特別是,由於閉包包含對 stat 的參照,Rust 不會讓它比 stat 活得更久。由於閉包只在排序期間使用,因此這個例子是正確的。
簡而言之,Rust 透過使用生命週期而不是垃圾回收來確保安全性。Rust 的方法更快:即使快速的 GC 分配也比將 stat 儲存在堆積疊上要慢,就像 Rust 在這種情況下所做的那樣。
竊取閉包(Closures That Steal)
第二個例子更棘手:
use std::thread;
fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
-> thread::JoinHandle<Vec<City>>
{
let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
thread::spawn(|| {
cities.sort_by_key(key_fn);
cities
})
}
這有點類別似於我們的 JavaScript 示例所做的:thread::spawn 接受一個閉包並在新的系統執行緒中呼叫它。注意 || 是閉包的空引數列表。
新執行緒與呼叫者平行執行。當閉包傳回時,新執行緒離開。(閉包的傳回值作為 JoinHandle 值發送回呼叫執行緒。我們將在第 19 章介紹這一點。)
同樣,閉包 key_fn 包含對 stat 的參照。但這次,Rust 無法保證該參照被安全使用。因此,Rust 拒絕了這個程式:
error[E0373]: closure may outlive the current function, but it borrows `stat`,
which is owned by the current function
閉包與執行緒安全:變數捕捉與函式型別
在 Rust 程式語言中,閉包(closure)是一種特殊的函式,它能夠捕捉其周圍環境中的變數。然而,當閉包被用於多執行緒環境中時,變數捕捉的方式就變得非常重要。本篇文章將討論 Rust 中的閉包如何捕捉變數,以及如何使用 move 關鍵字來確保執行緒安全。
變數捕捉的問題
當我們在 Rust 中建立一個閉包時,它預設會借用周圍環境中的變數。這在單執行緒環境中通常沒有問題,但是在多執行緒環境中,就可能導致問題。以下是一個例子:
fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
-> thread::JoinHandle<Vec<City>>
{
let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
thread::spawn(|| {
cities.sort_by_key(key_fn);
cities
})
}
內容解密:
- 上述程式碼嘗試在新執行緒中對
cities進行排序,但這裡有兩個問題:stat被借用但可能在閉包執行完之前就被銷毀。cities被多個執行緒分享,這是不安全的。
使用 move 關鍵字解決問題
為瞭解決上述問題,我們可以使用 move 關鍵字來告訴 Rust 將變數移動到閉包中,而不是借用它們。以下是修改後的程式碼:
fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
-> thread::JoinHandle<Vec<City>>
{
let key_fn = move |city: &City| -> i64 { -city.get_statistic(stat) };
thread::spawn(move || {
cities.sort_by_key(key_fn);
cities
})
}
內容解密:
move關鍵字使得stat被移動到key_fn閉包中,而cities和key_fn被移動到新執行緒的閉包中。- 這樣做確保了
stat和cities不會在新執行緒執行完之前被銷毀,從而避免了執行緒安全問題。
函式與閉包的型別
在 Rust 中,函式和閉包都可以被當作值來使用,因此它們都有自己的型別。例如:
fn city_population_descending(city: &City) -> i64 {
-city.population
}
這個函式的型別是 fn(&City) -> i64。我們可以將函式儲存在變數中,也可以將其作為引數傳遞給其他函式。
然而,閉包的型別與函式不同。閉包的型別取決於它捕捉的變數和它的引數列表。為了支援閉包,我們需要使用泛型和特殊的 trait,如 Fn(&City) -> bool。
fn count_selected_cities<F>(cities: &Vec<City>, test_fn: F) -> usize
where F: Fn(&City) -> bool
{
let mut count = 0;
for city in cities {
if test_fn(city) {
count += 1;
}
}
count
}
內容解密:
count_selected_cities函式接受一個閉包或函式作為引數,只要它滿足Fn(&City) -> bool的 trait 約束。- 這使得我們可以傳遞不同的閉包或函式給
count_selected_cities,從而實作了更大的靈活性。
閉包(Closures)的高效表現與安全特性
閉包是 Rust 程式語言中的一個重要概念,它允許我們建立可以捕捉周圍環境變數的匿名函式。Rust 的閉包設計旨在提供高效、安全且靈活的程式設計方式。
為何閉包的型別很重要
Rust 中的每個閉包都有其獨特的型別,這是因為閉包可能會捕捉周圍環境中的變數,並將其儲存為自己的資料。因此,每個閉包都有一個由編譯器生成的特定型別,這使得每個閉包的型別都不相同。但是,每個閉包都實作了一個 Fn 特徵(trait),這使得它們可以被呼叫。
count_selected_cities(&my_cities, has_monster_attacks); // ok
count_selected_cities(&my_cities, |city| city.monster_attack_risk > limit); // also ok
在上述例子中,第一個嘗試使用了一個函式 has_monster_attacks,而第二個嘗試使用了一個閉包。這兩個例子都能夠正常工作,因為 count_selected_cities 函式是泛型的,可以接受任何實作了 Fn 特徵的閉包。
內容解密:
count_selected_cities是一個泛型函式,可以接受任何實作了Fn特徵的閉包。- 閉包
|city| city.monster_attack_risk > limit捕捉了周圍環境中的limit變數,並實作了Fn(&City) -> i64特徵。 - 每個閉包都有其獨特的型別,但都實作了
Fn特徵,這使得它們可以被呼叫。
閉包的高效表現
Rust 的閉包設計旨在提供高效的表現。它們不像其他語言中的閉包那樣需要在堆積(heap)上分配記憶體,也不受限於垃圾回收(garbage collection)。這使得 Rust 的閉包在效能上與 C++ 的 lambda 表示式相當。
為何 Rust 的閉包高效
- 無垃圾回收:Rust 的閉包不需要垃圾回收,這減少了執行時的開銷。
- 型別明確:由於每個閉包都有其獨特的型別,Rust 編譯器可以在編譯時期就知道如何呼叫閉包,這使得呼叫閉包就像呼叫普通函式一樣高效。
- 內聯最佳化:Rust 編譯器可以對閉包進行內聯最佳化,這消除了函式呼叫的開銷,並使得其他最佳化成為可能。
圖示:閉包在記憶體中的佈局
此圖示展示了不同型別的閉包在記憶體中的佈局。
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust 閉包 綜觀變數捕捉與執行緒安全
package "安全架構" {
package "網路安全" {
component [防火牆] as firewall
component [WAF] as waf
component [DDoS 防護] as ddos
}
package "身份認證" {
component [OAuth 2.0] as oauth
component [JWT Token] as jwt
component [MFA] as mfa
}
package "資料安全" {
component [加密傳輸 TLS] as tls
component [資料加密] as encrypt
component [金鑰管理] as kms
}
package "監控審計" {
component [日誌收集] as log
component [威脅偵測] as threat
component [合規審計] as audit
}
}
firewall --> waf : 過濾流量
waf --> oauth : 驗證身份
oauth --> jwt : 簽發憑證
jwt --> tls : 加密傳輸
tls --> encrypt : 資料保護
log --> threat : 異常分析
threat --> audit : 報告生成
@enduml此圖示說明:
- 閉包(a)捕捉了周圍環境中的變數,並儲存為參照。
- 閉包(b)捕捉了周圍環境中的變數,並將其移動到自己的資料中。
- 閉包(c)沒有捕捉任何變數,因此不佔用任何記憶體。
安全性考量
Rust 的閉包與安全性密切相關。當一個閉包被建立時,它會捕捉周圍環境中的變數。這些變數可能會被移動或借用。瞭解閉包如何與 Rust 的所有權和生命週期系統互動對於寫出安全的程式碼至關重要。
FnOnce 特徵
有些閉包只能被呼叫一次,因為它們捕捉的變數在被呼叫時會被丟棄。嘗試多次呼叫這樣的閉包會導致編譯錯誤。FnOnce 特徵用於表示這樣的閉包。
fn call_twice<F>(closure: F) where F: Fn() {
closure();
closure();
}
內容解密:
call_twice函式接受任何實作了Fn()特徵的閉包。- 如果傳遞給
call_twice的是一個只能被呼叫一次的閉包,第二次呼叫將導致編譯錯誤。
總之,Rust 的閉包提供了一種高效、安全且靈活的方式來捕捉和操作周圍環境中的變數。瞭解閉包的工作原理和安全性考量對於寫出高品質的 Rust 程式碼至關重要。
閉包與安全性
在 Rust 程式語言中,閉包(Closures)是一種可以捕捉其環境的匿名函式。閉包在很多情況下非常有用,但也可能導致一些安全性問題。本章節將探討閉包的安全性議題。
閉包的種類別
Rust 中的閉包可以分為三種型別:Fn、FnMut 和 FnOnce。
Fn:可以被多次呼叫,不會改變其環境。FnMut:可以被多次呼叫,但可能會改變其環境。FnOnce:只能被呼叫一次,因為它會消耗其環境。
為什麼需要三種閉包?
Rust 需要三種閉包是因為閉包的安全性問題。當一個閉包捕捉了其環境中的變數時,它可能會對這些變數進行操作。如果多個執行緒同時呼叫同一個閉包,就可能會導致資料競爭(Data Race)。
FnOnce 閉包
FnOnce 閉包是隻能被呼叫一次的閉包。當一個閉包捕捉了一個變數並對其進行了操作,使得該變數被消耗(Consumed),那麼這個閉包就是 FnOnce。
let my_str = "hello".to_string();
let f = || drop(my_str);
call_twice(f); // 錯誤:f 是 FnOnce,不能被呼叫多次
在上述範例中,f 是一個 FnOnce 閉包,因為它捕捉了 my_str 並對其進行了 drop 操作。嘗試呼叫 call_twice(f) 將會導致編譯錯誤。
內容解密:
let my_str = "hello".to_string();:建立一個名為my_str的字串,內容為 “hello”。let f = || drop(my_str);:定義一個閉包f,它捕捉了my_str並對其進行了drop操作。由於my_str被消耗,因此f是FnOnce。call_twice(f);:嘗試呼叫f兩次,但由於f是FnOnce,因此這將導致編譯錯誤。
FnMut 閉包
FnMut 閉包是可以被多次呼叫的閉包,但它可能會改變其環境。
let mut i = 0;
let mut incr = || {
i += 1; // incr 借用了一個可變參照到 i
println!("Ding! i 現在是:{}", i);
};
call_twice(incr); // 原始的 call_twice 需要 Fn,但 incr 是 FnMut
在上述範例中,incr 是一個 FnMut 閉包,因為它捕捉了 i 並對其進行了修改。原始的 call_twice 需要 Fn,但 incr 是 FnMut,因此這將導致編譯錯誤。
如何修正?
為了修正上述錯誤,可以修改 call_twice 的定義,使其接受 FnMut 閉包:
fn call_twice<F>(mut closure: F) where F: FnMut() {
closure();
closure();
}
內容解密:
fn call_twice<F>(mut closure: F) where F: FnMut():定義一個名為call_twice的函式,它接受一個FnMut閉包。closure(); closure();:呼叫傳入的閉包兩次。
回呼(Callbacks)
許多函式庫使用回呼作為其 API 的一部分。回呼是一個由使用者提供的函式,供函式庫稍後呼叫。在 Rust 中,回呼通常是使用閉包來實作的。
let mut router = Router::new();
router.get("/", |_: &mut Request| {
Ok(get_form_response())
}, "root");
在上述範例中,傳遞給 router.get 的是一個閉包,它捕捉了其環境並傳回了一個回應。由於 Iron 框架被設計為接受任何執行緒安全的 Fn 作為引數,因此這段程式碼是有效的。