rust十三.1、匿名函式(閉包)

正在战斗中發表於2024-12-08

在編譯後,所謂的閉包是編譯為單獨的函式,所以原文的作者也把closure稱為函式。

因此,本文也稱為函式。這個更好理解的一個概念。

一、概念

在某個程式體內定義的一段程式碼,具有引數和程式體,但不具有名稱,實現函式的作用,這樣的程式碼稱為匿名函式(closure)。

匿名函式這個東西,現在各個語言大行其道,核心的原因是更加方便,某些習慣這樣思維的工程師能從此受益。在沒有匿名函式之前,程式也執行得好好的。

所以,所謂的匿名函式,本質上是編譯器的把戲!

二、定義方式

主要有兩種方式:

  1. 指定持有者變數方式
  2. 無持有者變數方式-這一種相當常見

但無論哪一種方式,在編碼上,不會給它們新增函式名稱,難以名之

2.1、指定持有者變數方式

let f = |x| { println!("不改變捕獲的x={}", x) };

2.2、無持有者變數方式

fn giveaway(&mut self, user_prefer:Option<ShirtColor>) -> ShirtColor {
user_prefer.unwrap_or_else(|| self.most_stocked())
}
函式unwrap_or_else中內容就是一個匿名函式.

2.3、和其它語言比較

前文說過,現在很多語言都有這種圖方便的寫法。
java有匿名函式和朗達表示式,javascript也有類似的匿名函式和朗達表示式。
和rust比起來,個人覺得還是java,js的書寫方式更加地人性化。例如java可以這樣寫:
Isort sort2 = (a, b) -> a + b;
Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from anonymous Runnable!");
            }
};

js可以這樣寫

// 帶有物件字面量的箭頭函式(注意:需要括號)
const createPerson = (name, age) => ({ name: name, age: age });
const person = createPerson("Alice", 30);
console.log(person); // 輸出: { name: 'Alice', age: 30 }
// 箭頭函式中的 this 繫結

function Person() {
    this.age = 0;
    setInterval(() => {
        this.age++; // `this` 繫結到 Person 例項
        console.log(this.age);
    }, 1000);
}

rust使用的是比較怪異的 ||替代(),並且引數定義區域和函式體之間只是用空格分隔,這種方式無疑會讓初學者誤會,也不太符合大部分語言的約定。

然而,刻意地與眾不同,應該是rust發明人追求目標。

只要這樣這個目標沒有影響到另外一個目標(安全、高效),那麼也還是可以忍受的!

三、變數捕獲和引用問題

如果有學過其它語言,這個其實很容易理解,其實麼有特別好理解的。

rust的麻煩主要是所有權和引用所導致的。

這些問題其實可以歸結為三個:

  1. 如何捕獲
  2. 影響之-如何使用:能不能修改
  3. 影響之-所有權

3.1、捕獲方式

一句話:自動捕獲

編譯器透過匿名函式的編寫和執行時候所使用的引數來確定捕獲了什麼變數。

直接在函式體捕獲外部變數

let mut z = 20;
let mut closure2 = || {
z += 1;
};

透過引數捕獲(實為傳參)

let fx=|y| y+1;
let v=20;
let r=fx(v);
println!("{}+1={}",v,r);
處於圖方便的緣故,第一種形式比較多,這一點在js中也是類似的。

3.2、可變捕獲

即不但捕獲,還要在匿名函式體內改變被捕獲變數的值。

如果採用”指定持有者變數“方式來定義此類匿名函式,那麼必須把為這個變數指定mut關鍵字,例如:

let mut z = 20;
let mut closure2 = || {
z += 1;
};
如果不是,則不需要。
可變捕獲後,有一個很特別的事情需要記住:一旦你使用了可變捕獲捕獲一個變數,那麼在最後一次匿名函式被呼叫之前,你不能在父級作用域使用被捕獲的變數
否則,編譯器會提示:immutable borrow occurs here.
或者提示 :cannot borrow `xxx` as mutable more than once at a time

3.3、所有權問題

在預設情況下,捕獲變數,不會導致所有權變化,只是純粹的引用:不可變引用,或者是可變引用。

大部分時候,我們只是希望借用下,就像大部分語言,函式那樣使用匿名函式。匿名還是在這種情況下,僅僅只是作為一個黑盒,有借有還!

如果你希望所有權轉移到匿名函式體內,那麼需要藉助關鍵字 move,例如:

let mut move_fn= move || {
vec.push(4);
println!("{:?}", vec);
};

以上一段程式碼中,匿名函式closure3擁有了vec的所有權。

當然奇特的不僅僅是在於這,而是你如果執行了類似move_fn一次後,還可以持續多次呼叫move_fn,而且vec的值會一直變化。

基於其它語言的概念和習慣,工程師們需要一段時間來適應這種有別於傳統的方式。

然而難度也不是那麼大,只是有一點而已。習慣它,只要知道編譯器就是這麼規定的,困難的事情是編譯器在做。

四、示例

fn main() {
    let x = 10;

    // 1. 沒有捕獲
    let closure1 = |y| x + y;
    println!("1.0執行前x={}", x); //x是不可變引用,所以x可以在父級作用域不停使用
    let x1=closure1(10);
    println!("1.0現在{}+{}={},x還是{}", x,10,x1,x); //x是不可變引用,所以x可以在父級作用域不停使用

    // 1.5 捕獲,但是不變
    let f = |x| { println!("不改變捕獲的x={}", x) };
    println!("1.5執行前x={}", x); //x是不可變引用,所以x可以在父級作用域不停使用
    f(x);
    println!("1.5現在x={}", x); //x是不可變引用,所以x可以在父級作用域不停使用

    //2.0 可變捕獲
    let mut z = 20;

    println!("2.0執行前,z={}", z);
    // 可變捕獲,實現FnMut
    let mut closure2 = || {
        z += 1;
    };
    closure2();
    //println!("第一次呼叫後,z={}", z);   //編譯錯誤 immutable borrow occurs here
    closure2();
    println!("2.0第二次呼叫後,z={}", z);

    // 3.0 所有權轉移的可變捕獲
    let mut vec = vec![1, 2, 3];

    println!("3.0執行前,vec={:?}", vec);
    // 所有權轉移,只實現FnOnce
    let mut closure3 = move || {
        vec.push(4);
        println!("{:?}", vec);
    };

    closure3();
    //println!("現在vec={:?}", vec);  //編譯錯誤,因為被closure3借用後,vec已經不在範圍之內(消失了);
    closure3();

    let  fx=|y| y+1;
    let v=20;
    let r=fx(v);
    println!("{}+1={}",v,r);
}

五、小結

  1. 匿名函式的確方便了新一代的工程師。但匿名函式在rust還是有大用的
  2. rust的所有權問題讓它的匿名函式和其它語言存在較大的不同
  3. 如果是可變捕獲,那麼會存在一個糟糕的,比較難於理解的現象:一旦你使用了可變捕獲捕獲一個變數,那麼在最後一次匿名函式被呼叫之前,你不能在父級作用域使用被捕獲的變數

而在其它語言中,不會有這個問題:因為我們都認為:定義是定義,都還有使用怎麼就捕獲了?

相關文章