進階函式與閉包

本節探索函式與閉包相關的進階特色,包括函式指標和回傳閉包。

函式指標

我們已探討過如何將閉包傳遞給函式,其實你還可以將一般的函式傳給函式!當你想要傳遞已經定義好的函式,而不是新的閉包時,就會凸顯這個技巧好用之處。函式將會轉型為 fn 型別(小寫的 f),別和閉包特徵的 Fn 搞混了。這個 fn 型別就稱為函式指標(function pointer)。你可以將函數作為函式指標傳遞,當作其他函式的引數。

將函式指標當作參數傳遞的語法,和閉包相當相似,如範例 19-27 所示。我們定義了函式 add_one,替其參數加一。函數 do_twice 接受兩個參數:一個函式指標,指向任何輸入 i32 且回傳 i32i32 value 的函式。do_twice 函式呼叫函式 f 兩次,並傳遞 arg 的數值,再將兩個函式呼叫的回傳值加總。main 函式以引數 add_one5 呼叫 do_twice

檔案名稱:src/main.rs

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("答案是:{}", answer);
}

範例 19-27:藉由 fn 型別接收函式指標引數

這段程式碼會印出 答案是:12。我們可以指定 do_twice 的參數 f 是一個需要一個 i32 當參數的 fn,並會回傳 i32。接下來我們在 do_twice 內呼叫 f。在 main 中,我們就可將 add_one 函式作為 do_twice 第一個引數。

和閉包不同的是,fn 不是特徵而是一個型別,所以我們可以直接將 fn 作為參數型別,而不需要宣告一個以 Fn 特徵作為特徵限制的泛型型別參數。

函式指標將 三個閉包特徵(FnFnMutFnOnce)通通實作了,意思是在預期要傳入閉包之處,你一定可以將函式指標作為引數傳進去。最佳的做法是寫一個同時使用泛型型別和其中一個閉包特徵的函式,這樣無論是函式還是閉包,你的函式全都可以接收。

也就是說,有個你只會想接收 fn 但不要閉包的例子,就是當你在與外部那些沒有閉包的程式碼打交道的時候,比如 C 可以接收函式作為引數,但 C 並沒有閉包。

讓我們來看一下標準函式庫中 Iterator 特徵提供的 map 的用法,map 就是可以用行內閉包(closure defined inline)或一個命名函式(named function)的例子。欲將數字的向量轉換成字串的向量,我們可以使用閉包,例如:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

或者,我們也可以將一個函式作為引數,代替閉包傳入 map

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

請注意,因為有多個可用的函式都叫做 to_string,所以我們必須使用先前在「進階特徵」一節提及的完全限定語法。這裡,我們使用了在 ToString 特徵中定義的 to_string 函式,只要有實作 Display 的型別,標準函式庫都會提供 ToString 的實作。

回想一下第六章「列舉數值」一節提及,我們定義的每個列舉變體的名字,也是一個初始化函式,我們可以將這些初始化函式當作實作了閉包特徵的函式指標,這就代表我們可以指定初始化函式作為引數,傳給需要閉包的方法,例如:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

這裡,我們對一個範圍呼叫 map,並用每個 u32 值,透過 Status::Value 的初始化函式來建立 Status::Value 的實例。有些人更喜歡上述的作法,但有人偏好閉包。這兩者的編譯結果相同,所以選一個你覺得清晰的風格吧。

回傳閉包

閉包是用特徵來表示,言下之意是你不能直接回傳一個閉包。大多數的情況,當你想回傳一個特徵時,可以改回傳有實作該特徵的具體型別。但你並無法對閉包這樣做,因為它們根本沒有可供回傳的具體型別,比方說不允許你使用 Fn 特徵作為回傳型別。

接下來的程式碼嘗試直接回傳一個閉包,但它無法編譯:

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}

編譯錯誤如下:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32`
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~~~~~~~~~~~~~~~~

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` due to previous error

這個錯誤再度指出 Sized 特徵!Rust 不知道我們需要多少空間儲存這個閉包,我們之前看過這類問題的解法。可以使用特徵物件:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

這段程式碼恰巧能通過編譯。欲知更多特徵物件相關資訊,請查閱第十七章「允許不同型別數值的特徵物件」部分。

接下來,讓我們來探討巨集吧!