Rust 閉包提供一種簡潔的匿名函式機制,能捕捉周圍環境變數,方便處理迭代器、執行緒和預設值計算等場景。然而,在多執行緒環境下,閉包的變數捕捉方式至關重要。Rust 沒有垃圾回收機制,而是透過借用和生命週期規則確保安全性。當閉包借用變數時,其生命週期受限於被借用變數的生命週期。若閉包的生命週期可能超過被借用變數,則需使用 move 關鍵字將變數所有權轉移至閉包,避免懸空指標等問題。理解 FnFnMutFnOnce 三種閉包型別,以及它們與變數捕捉方式的關聯,是確保執行緒安全的關鍵。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 根據閉包的使用方式推斷引數型別和傳回型別。

標準函式庫中其他接受閉包的範例如下:

  • 用於處理順序資料的迭代器方法,如 mapfilter。我們將在第 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 進行排序,但這裡有兩個問題:
    1. stat 被借用但可能在閉包執行完之前就被銷毀。
    2. 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 閉包中,而 citieskey_fn 被移動到新執行緒的閉包中。
  • 這樣做確保了 statcities 不會在新執行緒執行完之前被銷毀,從而避免了執行緒安全問題。

函式與閉包的型別

在 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 特徵的閉包。

內容解密:

  1. count_selected_cities 是一個泛型函式,可以接受任何實作了 Fn 特徵的閉包。
  2. 閉包 |city| city.monster_attack_risk > limit 捕捉了周圍環境中的 limit 變數,並實作了 Fn(&City) -> i64 特徵。
  3. 每個閉包都有其獨特的型別,但都實作了 Fn 特徵,這使得它們可以被呼叫。

閉包的高效表現

Rust 的閉包設計旨在提供高效的表現。它們不像其他語言中的閉包那樣需要在堆積(heap)上分配記憶體,也不受限於垃圾回收(garbage collection)。這使得 Rust 的閉包在效能上與 C++ 的 lambda 表示式相當。

為何 Rust 的閉包高效

  1. 無垃圾回收:Rust 的閉包不需要垃圾回收,這減少了執行時的開銷。
  2. 型別明確:由於每個閉包都有其獨特的型別,Rust 編譯器可以在編譯時期就知道如何呼叫閉包,這使得呼叫閉包就像呼叫普通函式一樣高效。
  3. 內聯最佳化: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();
}

內容解密:

  1. call_twice 函式接受任何實作了 Fn() 特徵的閉包。
  2. 如果傳遞給 call_twice 的是一個只能被呼叫一次的閉包,第二次呼叫將導致編譯錯誤。

總之,Rust 的閉包提供了一種高效、安全且靈活的方式來捕捉和操作周圍環境中的變數。瞭解閉包的工作原理和安全性考量對於寫出高品質的 Rust 程式碼至關重要。

閉包與安全性

在 Rust 程式語言中,閉包(Closures)是一種可以捕捉其環境的匿名函式。閉包在很多情況下非常有用,但也可能導致一些安全性問題。本章節將探討閉包的安全性議題。

閉包的種類別

Rust 中的閉包可以分為三種型別:FnFnMutFnOnce

  • 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) 將會導致編譯錯誤。

內容解密:

  1. let my_str = "hello".to_string();:建立一個名為 my_str 的字串,內容為 “hello”。
  2. let f = || drop(my_str);:定義一個閉包 f,它捕捉了 my_str 並對其進行了 drop 操作。由於 my_str 被消耗,因此 fFnOnce
  3. call_twice(f);:嘗試呼叫 f 兩次,但由於 fFnOnce,因此這將導致編譯錯誤。

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,但 incrFnMut,因此這將導致編譯錯誤。

如何修正?

為了修正上述錯誤,可以修改 call_twice 的定義,使其接受 FnMut 閉包:

fn call_twice<F>(mut closure: F) where F: FnMut() {
    closure();
    closure();
}

內容解密:

  1. fn call_twice<F>(mut closure: F) where F: FnMut():定義一個名為 call_twice 的函式,它接受一個 FnMut 閉包。
  2. closure(); closure();:呼叫傳入的閉包兩次。

回呼(Callbacks)

許多函式庫使用回呼作為其 API 的一部分。回呼是一個由使用者提供的函式,供函式庫稍後呼叫。在 Rust 中,回呼通常是使用閉包來實作的。

let mut router = Router::new();
router.get("/", |_: &mut Request| {
    Ok(get_form_response())
}, "root");

在上述範例中,傳遞給 router.get 的是一個閉包,它捕捉了其環境並傳回了一個回應。由於 Iron 框架被設計為接受任何執行緒安全的 Fn 作為引數,因此這段程式碼是有效的。