【譯】理解Rust中的閉包


原文標題:Understanding Closures in Rust
原文鏈接:https://medium.com/swlh/understanding-closures-in-rust-21f286ed1759
公眾號: Rust 碎碎念
翻譯 by: Praying

概要

  • 閉包(closure)是函數指針(function pointer)和上下文(context)的組合。
  • 沒有上下文的閉包就是一個函數指針。
  • 帶有不可變上下文(immutable context)的閉包屬於 Fn
  • 帶有可變上下文(mutable context)的閉包屬於 FnMut
  • 擁有其上下文的閉包屬於 FnOnce

理解 Rust 中不同類型的閉包

不同於其他語言,Rust 對self參數的使用是顯式的。當我們實現結構體時,必須把self作為函數簽名的第一個參數:

struct MyStruct {
    text: &'static str,
    number: u32,
}

impl MyStruct {
    fn new (text: &'static str, number: u32) -> MyStruct {
        MyStruct {
            text: text,
            number: number,
        }
    }

    // We have to specify that 'self' is an argument.
    fn get_number (&self) -> u32 {
        self.number
    }

    // We can specify different kinds of ownership and mutability of self.
    fn inc_number (&mut self) {
        self.number += 1;
    }

    // There are three different types of 'self'
    fn destructor (self) {
        println!("Destructing {}"self.text);
    }
}

因此,下面這兩種風格是一樣的:

obj.get_number();
MyStruct::get_number(&obj);

這和那些把self(或this)隱藏起來的語言不同。在那些語言中,只要將一個函數和一個對象或結構關聯起來,就隱含着第一個參數是self。在上面的代碼中,我們有四個self選項:一個不可變引用,一個可變引用,一個被擁有的值,或者壓根就沒有使用self作為參數。

因此,self表示函數執行的某一類上下文。它在 Rust 中是顯式的,但是在其他語言中經常是隱含的。

此外,在本文中,我們將會使用下面的函數:

fn is_fn <A, R>(_x: fn(A) -> R) {}
fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}

這些函數的唯一作用是類型檢查。例如,如果is_FnMut(&func)能夠編譯,那么我們就可以知道那個func屬於FnMut trait。

無上下文和fn(小寫的 f)類型

鑒於上述內容,考慮幾個使用(上面定義的)MyStruct的閉包的例子:

let obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
let closure1 = |x: &MyStruct| x.get_number() + 3;
assert_eq!(closure1(&obj1), 18);
assert_eq!(closure1(&obj2), 13);

這是我們可以得到的最簡單的(代碼示例)。這個閉包為類型MyStruct的任意對象中的已有數字加上三。它可以在任意位置被執行,不會有任何問題,並且編譯器不會給你帶來任何麻煩。我們可以很簡單地寫出下面這樣的代碼替代closure1:

// It doesn't matter what code appears here, the function will behave
// exactly the same.
fn func1 (x: &MyStruct) -> u32 {
    x.get_number() + 3
}
assert_eq!(func1(&obj1), 18);
assert_eq!(func1(&obj2), 13);

這個函數不依賴於它的上下文。無論它之前和之后發生了什么,它的行為都是一致的。我們(幾乎)可以互換着使用func1closure1

當一個閉包完全不依賴上下文時,我們的閉包的類型就是fn

// compiles successfully.
is_fn(closure1);
is_Fn(&closure1);
is_FnMut(&closure1);
is_FnOnce(&closure1);

可變上下文和Fn(大寫的 F)trait

相較於上面,我們可以為閉包添加一個上下文。

let obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
// obj1 is borrowed by the closure immutably.
let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
assert_eq!(closure2(&obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number();               // ERROR

closure2依賴於obj1的值,並且包含周圍作用域的信息。在這段代碼中,closure2將會借用obj1從而使得它可以在函數體中使用obj1。我們還可以對obj1進行不可變借用,但是如果我們試圖在后面修改obj1,我們將會得到一個借用錯誤。

如果我們嘗試使用fn語法來重寫我們的閉包,我們在函數內部需要知道的一切都必須作為參數傳遞給函數,所以我們添加了一個額外的參數來表示函數的上下文:

struct Context<'a>(&'a MyStruct);
let obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
let ctx = Context(&obj1);
fn func2 (context: &Context, x: &MyStruct) -> u32 {
    x.get_number() + context.0.get_number()
}

其行為和我們的閉包基本一致:

assert_eq!(func2(&ctx, &obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR

注意,Context結構體包含一個對MyStruct結構體的不可變引用,這表明我們將無法在函數內部修改它。

當我們調用closure1時,也意味着我們我們把周圍的上下文作為參數傳遞給了closure1,正如我們在使用fn時必須要做的那樣。在一些其他的語言中,我們不必指定將self作為參數傳遞,與之類似,Rust 也不需要我們顯式地指定將上下文作為參數傳遞。

當一個閉包以不可變引用的方式捕獲上下文,我們稱它實現了Fn trait。這表明我們可以不必修改上下文而多次調用我們的函數。

// Does not compile:
// is_fn(closure2);
// Compiles successfully:
is_Fn(&closure2);
is_FnMut(&closure2);
is_FnOnce(&closure2);

可變上下文和FnMut trait

如果我們在閉包內部修改obj1,我們會得到不同的結果:

let mut obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
// obj1 is borrowed by the closure mutably.
let mut closure3 = |x: &MyStruct| {
    obj1.inc_number();
    x.get_number() + obj1.get_number()
};
assert_eq!(closure3(&obj2), 26);
assert_eq!(closure3(&obj2), 27);
assert_eq!(closure3(&obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18);   // ERROR
// obj1.inc_number();                   // ERROR

這一次我們不能對obj1進行可變借用和不可變借用了。我們還必須得把閉包標注為mut。如果我們希望使用fn語法重寫這個函數,將會得到下面的代碼:

struct Context<'a>(&'a mut MyStruct);
let mut obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
let mut ctx = Context(&mut obj1);
// obj1 is borrowed by the closure mutably.
fn func3 (context: &mut Context, x: &MyStruct) -> u32 {
    context.0.inc_number();
    x.get_number() + context.0.get_number()
};

其行為與closure3相同:

assert_eq!(func3(&mut ctx, &obj2), 26);
assert_eq!(func3(&mut ctx, &obj2), 27);
assert_eq!(func3(&mut ctx, &obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18);       // ERROR
// obj1.inc_number();                       // ERROR

注意,我們必須把我們的上下文以可變引用的方式傳遞。這表明每次調用我們的函數后,可能會得到不同的結果。

當閉包以可變引用捕獲它的上下文時,我們稱它屬於FnMut trait。

// Does not compile:
// is_fn(closure3);
// is_Fn(&closure3);
// Compiles successfully:
is_FnMut(&closure3);
is_FnOnce(&closure3);

擁有的上下文

在我們的最后一個例子中,我們將會獲取obj1的所有權:

let obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
// obj1 is owned by the closure
let closure4 = |x: &MyStruct| {
    obj1.destructor();
    x.get_number()
};

我們必須在使用closure4之前檢查它的類型:

// Does not compile:
// is_fn(closure4);
// is_Fn(&closure4);
// is_FnMut(&closure4);
// Compiles successfully:
is_FnOnce(&closure4);

現在我們可以檢查它的行為:

assert_eq!(closure4(&obj2), 10);
// We can't call closure4 twice...
// assert_eq!(closure4(&obj2), 10);             //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15);           // ERROR
// obj1.inc_number();                           // ERROR

在這個例子中,我們只能調用這個函數一次。一旦我們對它進行了第一次調用,obj1就被我們銷毀了, 所以它在第二次調用的時候也就不復存在了。Rust 會給我們一個關於使用一個已經被移動的值的錯誤。這就是為什么我們要事先檢查其類型。

使用fn語法來實現,我們可以得到下面的代碼:

struct Context(MyStruct);
let obj1 = MyStruct::new("Hello"15);
let obj2 = MyStruct::new("More Text"10);
let ctx = Context(obj1);
// obj1 is owned by the closure
fn func4 (context: Context, x: &MyStruct) -> u32 {
    context.0.destructor();
    x.get_number()
};

這段代碼,正如我們所預期的,和我們的閉包行為一致:

assert_eq!(func4(ctx, &obj2), 10);
// We can't call func4 twice...
// assert_eq!(func4(ctx, &obj2), 10);             //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15);           // ERROR
// obj1.inc_number();                           // ERROR

當我們使用fn來寫我們的閉包時,我們必須使用一個Context結構體並擁有它的值。當一個閉包擁有其上下文的所有權時,我們稱它實現了FnOnce。我們只能調用這個函數一次,因為在調用之后,上下文就被銷毀了。

總結

  • 不需要上下文的函數擁有fn類型,並且可以在任意位置調用。

  • 僅需要不可變地訪問其上下文的函數屬於Fn trait,並且只要上下文在作用域中存在,就可以在任意位置調用。

  • 需要可變地訪問其上下文的函數實現了FnMut trait,可以在上下文有效的任意位置調用,但是每次調用可能會做不同的事情。

  • 獲取上下文所有權的函數只能被調用一次。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM