用閉包優化 Rust 代碼
在本文中,我們將探索並學習 Rust 中的閉包及其相關概念——例如,在 Rust 中使用帶有迭代器的閉包,或者 move 關鍵字如何獲得從閉包環境中捕獲的值的所有權。我們將研究 Rust 中使用閉包捕獲環境的關鍵技術,並通過示例演示如何使用閉包優化代碼。
Rust 中的閉包與函數
在 Rust 中,閉包和函數是用於不同目的的兩種不同類型的代碼塊。以下是 Rust 中閉包和函數之間需要了解的主要區別:
語法
Rust 定義閉包和函數的語法略有不同。
閉包使用語法 |parameters| -> return_type {body},其中參數在 | | 之間傳遞,主體被花括號 {} 包圍。
函數使用語法 fn name(parameters: type) -> return_type {body},其中參數在括號 () 之間傳遞,返回類型在箭頭 ->後指定。
主體
閉包的主體可以是一條語句,也可以是多條語句。如果閉包由單個語句組成,花括號是可選的;但是,如果閉包包含多條語句,則主體必須用花括號括起來。
相反,函數體必須總是用花括號括起來,無論它是由一條語句還是多條語句組成。
返回類型
在 Rust 中,閉包的返回類型是可選的——這意味着你不必指定閉包返回的值的類型。
函數的返回類型是強制的,你必須使用語法 -> return_type 指定函數返回的值類型。
數據類型
在 Rust 中,你不必在閉包中指定參數的數據類型。但是,必須使用類型參數語法指定函數中參數的數據類型。
捕獲環境變量
Rust 中的閉包可以從環境中捕獲變量,而函數則不能。這意味着閉包可以引用外部定義的變量。
在 Rust 中使用閉包捕獲環境變量
正如我們提到的,閉包可以從定義它們的環境中捕獲值——閉包可以借用或擁有這些周圍的值。
讓我們構建一個代碼場景,我們可以在 Rust 中執行一些環境變量的捕獲:
use std::collections::HashMap;
#[derive(Debug)]
struct Nft {
tokens: Option<HashMap<String, u32>>
}
fn main() {
let x = Nft {
tokens: Some(HashMap::from([(String::from("string"), 32)]))
};
let slice = vec![1, 3, 5];
let print_to_stdout = || {
println!("Slice: {:?}", slice);
if let Some(tokens) = &x.tokens {
println!("Nft supply --> {:?}", tokens);
}
};
print_to_stdout();
println!("{:?}", x);
print_to_stdout();
}
輸出結果如下:
Slice: [1, 3, 5]
Nft supply --> {"string": 32}
Nft { tokens: Some({"string": 32}) }
Slice: [1, 3, 5]
Nft supply --> {"string": 32}
在上面的代碼片段中,我們將 x 定義爲 Nft 結構體的一個實例。我們還定義了一個切片變量——類型爲 Vec。然後,我們定義了一個存儲在 print_to_stdout 變量中的閉包。
如果不將這兩個變量 (x 和 slice) 作爲參數傳遞到閉包中,我們仍然可以在 print_to_stdout 閉包中對它們進行不可變的訪問。
此外,由於 print_to_stdout 閉包只有一個對變量的不可變引用——這意味着它不能改變變量的狀態——我們可以多次調用閉包來打印值。
我們還可以重新定義閉包,通過稍微調整代碼片段來獲取變量的可變引用,如下所示:
// --snip--
fn main() {
// --snip--
let mut slice = vec![1, 3, 5];
let print_to_stdout = || {
slice.push(11);
// --snip--
println!("Slice: {:?}", slice);
};
print_to_stdout();
println!("{:?}", slice);
}
結果如下:
Slice: [1, 3, 5, 11]
[1, 3, 5, 11]
我們可以通過捕獲 slice 變量的可變引用來修改它的狀態。
在希望獲得周圍變量的所有權的情況下,可以在閉包旁邊使用 move 關鍵字。
// --snip--
fn main() {
// --snip--
//Redefined the closure using move keyword
let print_to_stdout = move || {
slice.push(11);
// --snip--
println!("Slice: {:?}", slice);
};
print_to_stdout();
}
現在,我們已經顯式地將變量移動到閉包中,獲得它們值的所有權。如果你試着調用 println!("{:?}”, slice);將得到一個錯誤,解釋變量由於在閉包中使用而被移動。
閉包作爲函數的參數
首先,讓我們看看 Fn 的三個特徵,因爲閉包會自動實現其中一個、兩個或全部三個特徵,這取決於函數簽名定義或函數體內容的性質。所有閉包至少都實現了 FnOnce 特性。
以下是對這三個特徵的解釋:
-
**FnOnce:**最多隻能被調用一次,任何將捕獲的變量返回到其調用環境的閉包都實現了此特性
-
**FnMut:**此特徵表示閉包可能會改變捕獲的值
-
**Fn:**此特徵表示閉包不會改變捕獲的值
當在項目中爲不同用途定義閉包時,這些規則可以作爲我們的指路明燈。讓我們通過實現上面提到的特徵之一來演示一個示例:
#[derive(Debug)]
enum State<T> {
Received(T),
Pending,
}
impl<T> State<T> {
pub fn resolved<F>(self, f: F) -> T
where F: FnOnce() -> T
{
match self {
State::Received(v) => v,
State::Pending => f(),
}
}
}
fn main() {
let received_state = State::Received(String::from("LogRocket"));
println!("{:?}", received_state.resolved(|| String::from("executed closure")));
let pending_state = State::Pending;
println!("{:?}", pending_state.resolved(|| String::from("executed closure")))
}
結果輸出:
"LogRocket"
"executed closure"
在上面的代碼段中,我們創建了一個示例 State 枚舉,以假設表示一個網絡調用,該網絡調用的完成狀態爲 Received(T) 和 Pending 狀態。在枚舉上,我們實現了一個函數來檢查網絡調用的狀態並進行相應的操作。
查看函數簽名,你將注意到函數的 f 參數是一個泛型參數:FnOnce。使用特徵綁定 (F: FnOnce() -> T),我們爲 F 定義了可能的參數值,這意味着 F 最多隻能被調用一次,不接受自己的參數,並返回 T 的泛型值。
當狀態恰好是 Pending 時,閉包參數將被調用。
使用帶有迭代器的閉包來處理集合
在本節中,你將學習閉包最常見的用例之一:使用帶有迭代器的閉包來處理集合中的一系列順序數據。
讓我們通過首先定義一個 vector 變量來進一步解釋閉包如何與迭代器一起工作:
#[derive(PartialEq, Debug)]
struct MusicFile {
size: u32,
title: String,
}
fn main() {
let files = vec![
MusicFile {
size: 1024,
title: String::from("Last last"),
},
MusicFile {
size: 2048,
title: String::from("Influence"),
},
MusicFile {
size: 1024,
title: String::from("Ye"),
},
];
let max_size = 1024;
let accepted_file_sizes: Vec<MusicFile> = files.into_iter().filter( |s| s.size == max_size).collect();
println!("{:?}", accepted_file_sizes);
}
結果輸出:
[MusicFile { size: 1024, title: "Last last" }, MusicFile { size: 1024, title: "Ye" }]
在上面的代碼片段中,我們使用 into_iter 方法將 files 變量改編爲迭代器,該方法可以在 Vec 類型上調用。
into_iter 方法創建迭代器類型,該迭代器將每個值移出 files 變量 (從 start 到 finish),這意味着在調用該變量後不能再使用它。
我們直接在 filter 函數中以閉包作爲參數,然後調用 collect() 來消費迭代器並將其轉換回 Rust 集合 Vec 類型。
總結
閉包是與普通函數或迭代器一起使用的類似函數的構造,你可以實現特定的閉包類型,這取決於如何使用它的上下文 - 捕獲變量的所有權或借用變量的引用,或者兩者都不!
本文翻譯自:
https://blog.logrocket.com/optimizing-rust-code-with-closures-environment-capturing/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TnPm75SBkyvSM_G0k3AXQw