【譯】理解Rust中的閉包

Praying發表於2020-11-12

原文標題: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,可以在上下文有效的任意位置呼叫,但是每次呼叫可能會做不同的事情。

  • 獲取上下文所有權的函式只能被呼叫一次。

相關文章