Rust函數與閉包

来源:https://www.cnblogs.com/leometeor/archive/2023/09/25/17693693.html
-Advertisement-
Play Games

1. 常規函數 函數都擁有顯示的類型簽名,其本身也是一種類型。 1.1 函數類型 自由函數 // 自由函數 fn sum(a: i32, b: i32) -> i32 { a+b } fn main() { assert_eq!(3, sum(1, 2)) } 關聯函數與方法 struct A(i3 ...


1. 常規函數

函數都擁有顯示的類型簽名,其本身也是一種類型。

1.1 函數類型

自由函數

// 自由函數
fn sum(a: i32, b: i32) -> i32 {
    a+b
}
fn main() {
    assert_eq!(3, sum(1, 2))
}

關聯函數與方法

struct A(i32, i32);
impl A {
    // 關聯函數
    fn sum(a: i32, b: i32) -> i32 {
        a+b
    }
    // 方法: 第一個參數是self, &self或&mut self的函數
    fn math(&self) -> i32 {
        Self::sum(self.0, self.1)
    }
}

fn main() {
    let a = A(1, 2);
    assert_eq!(3, A::sum(1, 2));
    assert_eq!(3, a.math());
}

1.2 函數項類型

struct A(i32, i32);
impl A {
    // 關聯函數
    fn sum(a: i32, b: i32) -> i32 {
        a+b
    }
    // 方法: 第一個參數是self, &self或&mut self的函數
    fn math(&self) -> i32 {
        Self::sum(self.0, self.1)
    }
}

fn main() {
    let a = A(1, 2);
    let add = A::sum;  // Fn item type
    let add_math = A::math;  // Fn item type
    assert_eq!(add(1, 2), A::sum(1, 2));
    assert_eq!(add_math(&a), a.math());
}

函數項類型是一個零大小的類型,會在類型中記錄函數的相關信息。
枚舉類型與元組結構體類型與函數項類型一樣,都是零大小類型。

enum Color {
    R(i16),
    G(i16),
    B(i16),
}
// 等價於
// fn Color::R(_1: i16) -> Color { /* ...  */}
// fn Color::G(_1: i16) -> Color { /* ...  */}
// fn Color::B(_1: i16) -> Color { /* ...  */}
fn main() {
    println!("{:?}", std::mem::size_of_val(&Color::R)); // 0
}

這段代碼中Color::R是一個類型構造體,等價於一個函數項。
Rust預設為函數項實現了一些trait:Copy, Clone, Sync, Send, Fn, FnMut, FnOnce

2. 函數指針

函數存放在記憶體的代碼區域內,它們同樣有地址。可以使用函數指針來指向要調用的函數的地址,將函數指針傳入函數中,就可以實現將函數本身作為函數的參數。

這樣傳遞函數的方式在C語言中非常常見。

type RGB = (i16, i16, i16);
fn color(c: &str) -> RGB {
    (1, 1, 1)
}
// 這裡的參數類型fn(&str)->RGB是函數指針類型, fn pointer type
fn show(c: fn(&str)->RGB) {
    println!("{:?}", c("black"));
}

fn main() {
    let rgb = color;  // rgb屬於函數項類型
    show(rgb); // (1, 1, 1), 這裡發生了函數項類型到函數指針類型的隱式轉換
}

上述代碼中rgb是一個函數項,屬於函數項類型(Fn item type),而show函數的參數則是一個函數指針類型(Fn pointer type)

fn main() {
    let rgb = color;  // 函數項類型
    let c: fn(&str)->RGB = rgb;  // 隱式轉換為了函數指針類型
    println!("{:?}", std::mem::size_of_val(&rgb));  // 0
    println!("{:?}", std::mem::size_of_val(&c));  // 8
}

應該儘可能使用函數項類型,這樣有助於享受零大小類型的優化。

3. 閉包

閉包可以捕獲環境變數,而函數則不可以。

// 以下代碼在Rust中會報錯
fn foo() -> fn(u32) {
    let msg: String = "hello".to_string();
    fn bar(n: u32) {
        for _i in 0..n {
            println!("{}", msg);
        }
    }
    return bar;
}

fn main() {
    let func = foo();
    func(5);
}

以上代碼foo函數中定義的bar函數使用了環境變數msg,然而在內部函數中使用這個環境變數是被編譯器所禁止的,編譯器會編譯報錯。這是因為Rust定義函數的語法無法指定如何捕獲環境變數,因此內部定義的函數無法在編譯時判斷使用的環境變數的生命周期是否合法,也因此Rust不允許在內部函數中使用環境變數。

想要實現以上功能就需要使用閉包。閉包在Rust中其實是一種語法糖,閉包的寫法如下所示

fn foo() -> impl Fn(u32) -> () {
    let msg: String = "hello".to_string();
    let bar = move |n: u32| -> () {
        for _i in 0..n {
            println!("{}", msg);
        }
    };
    return bar;
}

fn main() {
    let func = foo();
    func(5);
}

bar是一個完整的閉包定義,其中move關鍵字表示捕獲的環境變數所有權會被轉義到閉包內,|n|是閉包的參數,-> ()表示返回值類型, {...}內是閉包的具體代碼,Rust的閉包並不需要指定需要捕獲的變數,閉包中使用到的環境變數會被自動捕獲。Rust捕獲環境變數預設是獲取環境變數的引用,當使用了move關鍵字時,則強制捕獲環境變數本身,這也就導致了所有權的轉移。

3.1 閉包語法糖

Rust的閉包,實際上是語法糖,它本質上是一個實現了特定trait的匿名的struct,與閉包相關的trait有這三個:

  • Fn
  • FnMut
  • FnOnce

因此以上這種閉包代碼它可以被展開為如下代碼:

#![feature(unboxed_closures)]

fn foo() -> impl Fn(u32) -> () {
    let msg: String = "hello".to_string();

    struct ClosureEnvironment {
        env_var: String,
    }

    impl FnOnce<(u32, )> for ClosureEnvironment {
        type Output = ();

        extern "rust-call" fn call_once(self, args: (u32, )) -> Self::Output {}
    }

    impl FnMut<(u32, )> for ClosureEnvironment {
        extern "rust-call" fn call_mut(&mut self, args: (u32, )) -> Self::Output {}
    }

    impl Fn<(u32, )> for ClosureEnvironment {
        extern "rust-call" fn call(&self, args: (u32, )) -> Self::Output {
            let ClosureEnvironment { env_var } = self;
            for _i in 0..args.0 {
                println!("{}", env_var);
            }
        }
    }

    ClosureEnvironment { env_var: msg }
}

fn main() {
    let func = foo();
    func(5);
}

使用這個展開後的代碼,就可以理解閉包前的move關鍵字的作用了,使用了move後,ClosureEnvironment結構體中的環境變數env_var保存的是String對象本身,而非引用,向其中傳遞環境變數msgmsg的所有權就被轉移到了閉包內部。如果不使用move關鍵字,Rust的閉包預設會將引用傳遞到閉包的結構體內,而不是轉移環境變數的所有權。

3.2 閉包的類型

閉包實現的trait可以為一下三種類型:

FnOnce類型

正如上面閉包展開代碼所示,實現FnOnce trait中的call方法時,第一個參數的類型是self對象本身,這就會消耗閉包結構體,這也就是為什麼這種閉包只能調用一次。
編譯器把FnOnce的閉包類型看成函數指針。

FnMut類型

正如上面閉包展開代碼所示,實現FnMut trait中的call方法時,第一個參數的類型是&mut self,是閉包對象的可變借用,不會消耗閉包結構體,切閉包函數可以對環境變數進行修改,可以被多次調用。

Fn類型

正如上面閉包展開代碼所示,實現Fn trait中的call方法時,第一個參數的類型是& self,是閉包對象的不可變借用,不會消耗閉包結構體,閉包函數不可以對環境變數進行修改,可以被多次調用。

  1. Fn: applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

  2. FnMut: applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.

  3. FnOnce: applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits, because it can only be called once.

3.3 逃逸閉包與非逃逸閉包

如果使用閉包的作用域與定義閉包的作用域不同時,稱該閉包為逃逸閉包,否則為非逃逸閉包
通常如果一個函數返回值為閉包類型,則該閉包就為逃逸閉包。
逃逸閉包會遇到一個問題:如果閉包捕獲了環境變數,閉包又離開了定義它的作用域,這時如果環境變數沒有move或者copy到閉包中,則會出現閉包引用了原作用域中已回收變數的問題。
因此如果需要將閉包作為函數的返回值時,需要使用move將環境變數的所有權轉移到閉包中,確保環境變數的生命周期在函數調用結束時不會結束。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 服務業務線:快遞、快運、中小件、大件、冷鏈、國際、B2B合同物流、CLPS、京喜、三入三出(採購入、退貨入、調撥入、銷售出、退供出、調撥出)等 ...
  • 零售企業的發展路徑 零售企業的發展路徑一般可分為以下幾個階段: 單店經營階段:企業在一個地區或城市開設單個門店。這時,企業需要把精力放在瞭解當地市場和顧客需求上,這是積累經驗和品牌知名度的重要環節。為了在市場中建立競爭力,企業需要不斷提升產品和服務的質量,比如探索新的零售方式、創新商品、提高服務質量 ...
  • 一、引言 本文是京東到家自動化測試體系建設過程中的一些回顧和總結,刪減了部分系統設計與實踐的章節,保留了組織與文化相關的內容,整理成文,以饗讀者。 下麵就以QA(Quality Assurance)的視角來探討工作中經常面臨的問題與挑戰。 關於軟體質量,不知道你有沒有以下困惑: 西醫中“頭疼醫頭,腳 ...
  • 題目:員工工資單計算器 描述: 請編寫一個Python程式,該程式將通過用戶輸入來計算並列印員工的工資單。工資單應該包括員工的姓名、工作時長、每小時工資、毛工資、扣除額和凈工資。扣除額包括稅款和養老金。 要求: 1. 輸入: 員工姓名(字元串) 工作時長(整數,單位:小時) 每小時工資(浮點數,單位 ...
  • 在JDK 21中,Sequenced Collections的引入帶來了新的介面和方法來簡化集合處理。此增強功能旨在解決訪問Java中各種集合類型的第一個和最後一個元素需要非統一且麻煩處理場景。 下麵一起通過本文來瞭解一下不同集合處理示例。 Sequenced Collections介面 Seque ...
  • extern extern 是 C++ 中的一個關鍵字,用於聲明一個變數或函數是在其他文件中定義的。它的作用是告訴編譯器在鏈接時在其他文件中尋找該變數或函數的定義。 在 C++ 中,如果一個變數或函數在多個文件中使用,那麼就需要在每個文件中都聲明一次該變數或函數。這時就可以使用 extern 關鍵字 ...
  • 前言 最近寫一個任務隊列,可以支持存入返回值為void的任意函數對象。需要定義一個Task模板,來存儲函數對象以及參數。大致的實現如下: class Task { public: template <typename Func, typename... Args> Task(Func&& f, Ar ...
  • Python中有三種數字類型: int(整數) float(浮點數) complex(複數) 當您將值分配給變數時,將創建數字類型的變數: 示例:獲取您自己的Python伺服器 x = 1 # int y = 2.8 # float z = 1j # complex 要驗證Python中任何對象的類 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...