用閉包優化 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 {
    tokensOption<HashMap<String, u32>>
}

fn main() {
    let x = Nft {
        tokensSome(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 特性。

以下是對這三個特徵的解釋:

當在項目中爲不同用途定義閉包時,這些規則可以作爲我們的指路明燈。讓我們通過實現上面提到的特徵之一來演示一個示例:

#[derive(Debug)]
enum State<T> {
    Received(T),
    Pending,
}

impl<T> State<T> {
    pub fn resolved<F>(self, fF) -> T
    where FFnOnce() -> 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 {
    sizeu32,
    titleString,
}

fn main() {
    let files = vec![
            MusicFile {
                size1024,
                titleString::from("Last last"),
            },
            MusicFile {
                size2048,
                titleString::from("Influence"),
            },
            MusicFile {
                size1024,
                titleString::from("Ye"),
            },
        ];

        let max_size = 1024;

        let accepted_file_sizesVec<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