Try   HackMD

Crust of Rust : Functions, Closures, and Their Traits

直播錄影

  • 主機資訊
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> neofetch --stdout
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx 
    ​​​​----------------------------------------------- 
    ​​​​OS: Ubuntu 22.04.3 LTS x86_64 
    ​​​​Host: HP Pavilion Plus Laptop 14-eh0xxx 
    ​​​​Kernel: 6.2.0-37-generic 
    ​​​​Uptime: 22 mins 
    ​​​​Packages: 2367 (dpkg), 11 (snap) 
    ​​​​Shell: bash 5.1.16 
    ​​​​Resolution: 2880x1800 
    ​​​​DE: GNOME 42.9 
    ​​​​WM: Mutter 
    ​​​​WM Theme: Adwaita 
    ​​​​Theme: Yaru-dark [GTK2/3] 
    ​​​​Icons: Yaru [GTK2/3] 
    ​​​​Terminal: gnome-terminal 
    ​​​​CPU: 12th Gen Intel i5-12500H (16) @ 4.500GHz 
    ​​​​GPU: Intel Alder Lake-P 
    ​​​​Memory: 8569MiB / 15695MiB 
    
  • Rust 編譯器版本 :
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> rustc --version
    ​​​​rustc 1.70.0 (90c541806 2023-05-31) (built from a source tarball)
    

Introduction

0:00:00

In this episode, we go over the differences between function items, function pointers, and closures, as well as how they interact with the Fn* traits. We also touch upon dynamically dispatched Fns and experimental const Fn bounds.

Rust for Rustaceans

0:01:20

Jon 寫了 Rust for Rustaceans,讀這本書之前必須先有先備知識,至少要看過 The Rust Programming Language

Function items

0:02:48

開始建置 Rust 專案 :

$ cargo new --bin call-me
$ cd eksempel
$ vim src/main.rs

看到主程式 :

fn main() { // main 是個函式,main 也有型別 println!("Hello, world!"); }

觀察 bar 函式 :

fn main() { println!("Hello, world!"); let x = bar; // x 的型別為 fn bar(),看似函式指標,實則並不然。 // 它實際上是 function item,function item 與函式指標有點不同。 // 首先 function item 在編譯時期僅用於參考唯一函式 bar 的 0-sized 值。 } fn bar() {}

如果 bar 函式為泛型,我們就不能將 bar 函式指派給變數 :

fn main() { println!("Hello, world!"); let x = bar; } -fn bar() {} +fn bar<T>() {}
目前程式碼
fn main() { println!("Hello, world!"); let x = bar; } fn bar<T>() {}

編譯出現以下錯誤 :

$ cargo run
...
error[E0282]: type annotations needed
 --> src\main.rs:3:13
  |
3 |     let x = bar; 
  |             ^^^ cannot infer type of the type parameter `T` declared on the function `bar`
  |
help: consider specifying the generic argument
  |
3 |     let x = bar::<T>; 
  |                +++++

編譯器有提示你需要傳入泛型參數。

嘗試重新將 x 指向使用不同泛型參數 instantiate 的 function item :

fn main() { println!("Hello, world!"); - let mut x = bar; + let mut x = bar::<i32>; x = bar::<u32>; } fn bar<T>() {}
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; x = bar::<u32>; } fn bar<T>() {}

編譯器會提示錯誤 :

$ cargo run
...
error[E0308]: mismatched types
 --> src\main.rs:4:9
  |
3 |     let mut x = bar::<i32>;
  |                 ---------- expected due to this value
4 |     x = bar::<u32>;
  |         ^^^^^^^^^^ expected `i32`, found `u32`
  |
  = note: expected fn item `fn() {bar::<i32>}`
             found fn item `fn() {bar::<u32>}`
  = note: different fn items have unique types, even if their signatures are the same
  = help: consider casting both fn items to fn pointers using `as fn()`

會有這個錯誤是因為,x 並不是函式指標,而是 function item。

實際印出 x 占用的記憶體空間 :

fn main() { println!("Hello, world!"); let mut x = bar; + println!("{}", std::mem::size_of_val(&x)); - x = bar::<u32>; + // x = bar::<u32>; } fn bar<T>() {}
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; } fn bar<T>() {}

編譯並執行程式真的得到 0-sized :

$ cargo run ... Hello, world! 0

x 根本不持有指標,因為 bar::<i32> 只是編譯器用來識別此函式的唯一 instance 的 identifier。

Function pointers

0:06:26

Function item 具有為它們定義的強制轉換為函式指標 :

fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; + baz(bar::<i32>); + baz(bar::<u32>); } -fn bar<T>() {} +fn bar<T>(_: u32) -> u32 { 0 } +fn baz(f: fn(u32) -> u32 ) {}

baz 接收的型別為函式指標,這時的 Line 6 與 Line 7 編譯器會將它們從 function item 強制轉換成函式指標。

驗證是否轉為函式指標,印出 baz 接收到的參數所占的記憶體大小 :

fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<u32>); baz(bar::<i32>); } fn bar<T>(_: u32) -> u32 { 0 } -fn baz(f: fn(u32) -> u32 ) {} +fn baz(f: fn(u32) -> u32 ) { + println!("{}", std::mem::size_of_val(&f)); +}
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); } fn bar<T>(_: u32) -> u32 { 0 } fn baz(f: fn(u32) -> u32 ) { println!("{}", std::mem::size_of_val(&f)); }

編譯並執行可以看到編譯器真的將 function item 強制轉換成函式指標 :

$ cargo check ... Hello, world! 0 8 8

這個轉換是必要的,因為這兩個函式為不同的型別的 bar 函式 instantiations,這意味著它們具有不同的程式碼,它們已經被單獨最佳化了,它們是不同的程式碼塊,恰好共享一個泛用名稱。

當我們第一次呼叫 baz 函式時,我們需要將指標傳遞給函式的一個 instance 的第一個指令,而第二次呼叫 baz 函式時,則需要傳遞一個不同的函式指標。

fn baz(f: fn(u32) -> u32 ) 確實允許我們對傳入的確切函式進行泛型化,只要它具有相同的簽章,若將 bar 的函式簽章做修改將會產生編譯錯誤 :

fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); } -fn bar<T>(_: u32) -> u32 { 0 } +fn bar<T>() {} fn baz(f: fn(u32) -> u32 ) { println!("{}", std::mem::size_of_val(&f)); }

編譯出現以下錯誤 :

$ cargo run ... error[E0308]: mismatched types --> src\main.rs:6:9 | 6 | baz(bar::<i32>); | --- ^^^^^^^^^^ incorrect number of function parameters | | | arguments to this function are incorrect | = note: expected fn pointer `fn(u32) -> u32` found fn item `fn() {bar::<i32>}` ...

我們從中學到的是,function item 和函式指標彼此不同,但 function item 可以強制轉換為函式指標。編譯器這麼做的原因如下 :

fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); } fn bar<T>(_: u32) -> u32 { 0 } fn baz(f: fn(u32) -> u32 ) { println!("{}", std::mem::size_of_val(&f)); }

如果沒有在 Line 6 與 Line 7 呼叫 baz 函式,即使我們將具 Tbar 函式 instantiat 為 i32,編譯器實際上並不需要產生該函式的程式,它並沒有義務這樣做,因為 bar::<i32> 從未被呼叫。

如果在 Line 6 與 Line 7 呼叫 baz 函式,編譯器將此強制轉換為指標的原因是,它需要能夠產生一個指向該函式 body 的指標值。因此,編譯器必須產生該函式的程式碼,以便首先產生該函式指標。

結論 : Function item 唯一標識函式的特定 instance,而函式指標則是指向具有特定簽章的函式的指標,你可以將一個 function item 轉換為函式指標,但不能反向進行轉換。

目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); }

Function traits

0:11:24

接著看到 Fn() :

fn main() { ... quox(bar::<u32>); } fn bar<T>() {} ... fn quox<F>(f : F) where F: Fn(), { }

Fn()fn() 是不一樣的東西,雖然它們的建構方式相同。fn() 作為函式指標使用,Fn() 則是 trait。

  • Trait std::ops::Fn 的簽章如下 :
    ​​​​pub trait Fn<Args>: FnMut<Args> ​​​​where ​​​​ Args: Tuple, ​​​​{ ​​​​ // Required method ​​​​ extern "rust-call" fn call(&self, args: Args) -> Self::Output; ​​​​ // 傳入 self 的參考 ​​​​}

Fn() 在某種程度上相當於擁有共享參考,因為你可以多次呼叫它,並且你可以透過共享參考同時多次呼叫它,或者至少是透過一個共享參考。

  • Trait std::ops::FnMut 的簽章如下 :
    ​​​​pub trait FnMut<Args>: FnOnce<Args> ​​​​where ​​​​ Args: Tuple, ​​​​{ ​​​​ // Required method ​​​​ extern "rust-call" fn call_mut( ​​​​ &mut self, ​​​​ args: Args ​​​​ ) -> Self::Output; ​​​​ // 傳入 self 的 exclusive 參考 ​​​​}
    FnMut() 的話,你一次只能呼叫一次。所以,首先你需要一個 mutable 參考。如果你有一個 mutable 參考,你可以多次呼叫它,但一次只能呼叫一次,這點很重要。例如,如果你將 FnMut() 插入 Rc,則將無法呼叫它。同樣,如果你提供了對 FnMut() 的共享參考,你也無法呼叫它。這也是為什麼一般情況下,如果你透過 rayon 之類的東西將它傳遞給多個執行緒,那麼你就不能同時為多個執行緒呼叫 FnMut,因為它不是 exclusive。
  • Trait std::ops::FnOnce 的簽章如下 :
    ​​​​pub trait FnOnce<Args> ​​​​where ​​​​ Args: Tuple, ​​​​{ ​​​​ type Output; ​​​​ // Required method ​​​​ extern "rust-call" fn call_once(self, args: Args) -> Self::Output; ​​​​ // 傳入 owned self ​​​​}
    FnOnce() 的命名得知,你只能呼叫它一次。你已經移動了你想要呼叫的函式的值,你再也無法再次呼叫它。

一旦我們了解什麼是 closures 以及它們的作用,原因就會變得清晰。讓我們先具體考慮一下這對於 function item 和函式指標意味著什麼。

function item 和函式指標並沒有狀態,它們只是獨立的程式碼區塊,並且不參考任何其他 stack frame 中的任何內容。它們不參考儲存在自身外部的任何記憶體。它們不與生命週期相關聯,這意味著它們實際上不關心 self。對於 function item 來說,self 中不包含任何東西。對於函式指標來說,self 只是指向函式的指標。但實際上沒有狀態可變。沒有東西可移動。

因此,function item 強制轉換為函式指標,而函式指標實作了 Fn/ FnMut/ FnOnce trait。因此,如果你有一個函式指標,則可以將其傳遞給接受 Fn/ FnMut/ FnOnce 的函式。

這些 Fn trait 的一種思考方式是,它們是一種階層結構 :
image
對應文件 :

Since both Fn and FnMut are subtraits of FnOnce, any instance of Fn or FnMut can be used where a FnOnce is expected.

根據文件的敘述可以得知 FnOnce 實作了 Fn :

impl<F> FnOnce for F where F: Fn(), { fn call(self) {} }

我們只談到了函式指標,但如果我們有一個對舊 owned self 的參考,或者一個 owned self,那麼我們可以輕而易舉地產生一個對 self 的參考。因此,我們可以輕鬆地將 self 作為 Fn 呼叫。就像我可以輕鬆地將一個轉換為另一個一樣。因為如果我有一個實作了 Fn 的東西,並且我有一個 self,我只需將其取為參考並將其傳遞給 Fn :

impl<F> FnOnce for F where F: Fn(), { fn call(self) { Fn::call(&self); } }

FnOnce 也可以實作出 FnMut :

impl<F> FnOnce for F where F: FnMut(), { fn call(mut self) { FnMut::call(&mut self); } }

FnMut 則可以實作出 Fn :

impl<F> FnMut for F where F: Fn(), { fn call(&mut self) { Fn::call(&*self); } }

函式指標實作了 Fn/ FnMut/ FnOnce trait。因為它實際上不關心函式指標的所有權,因為它只是指向程式碼的指標。所以前面的 quox 函式的 bound 可以是 Fn/ FnMut/ FnOnce 其中一個。

  • 如果是 quox 函式使用到 FnOnce bound,若傳入 exclusive 參考,你不能呼叫 f,因為你並沒有 f 的擁有權 :
    ​​​​fn main() ​​​​{ ​​​​ ... ​​​​ quox(&mut bar::<u32>); ​​​​} ​​​​fn quox<F>(f : &mut F) ​​​​ where F: FnOnce(), ​​​​{ ​​​​ (f)() ​​​​}
    目前程式碼
    ​​​​fn main() ​​​​{ ​​​​ println!("Hello, world!"); ​​​​ let mut x = bar::<i32>; ​​​​ println!("{}", std::mem::size_of_val(&x)); ​​​​ // x = bar::<u32>; ​​​​ baz(bar::<i32>); ​​​​ baz(bar::<u32>); ​​​​ quox(&mut bar::<u32>); ​​​​} ​​​​fn bar<T>() {} ​​​​fn baz(f: fn()) { ​​​​ println!("{}", std::mem::size_of_val(&f)); ​​​​} ​​​​fn quox<F>(f : &mut F) ​​​​ where F: FnOnce(), ​​​​{ ​​​​ (f)() ​​​​}
    編譯會出現以下錯誤,因為傳入的並不是 owned,只是 mutable 參考 :
    ​​​​$ cargo run ​​​​... ​​​​error[E0507]: cannot move out of `*f` which is behind a mutable reference ​​​​ --> src\main.rs:29:5 ​​​​ | ​​​​29 | (f)() ​​​​ | ^^^-- ​​​​ | | ​​​​ | `*f` moved due to this call ​​​​ | move occurs because `*f` has type `F`, which does not implement the `Copy` trait ​​​​...
    如果你將 bound 改為 FnMut 就可以呼叫 f :
    ​​​​fn main() ​​​​{ ​​​​ ... ​​​​ quox(&mut bar::<u32>); ​​​​} ​​​​fn quox<F>(f : &mut F) ​​​​ where F: FnMut(), ​​​​{ ​​​​ (f)() ​​​​}
    目前程式碼
    ​​​​fn main() ​​​​{ ​​​​ println!("Hello, world!"); ​​​​ let mut x = bar::<i32>; ​​​​ println!("{}", std::mem::size_of_val(&x)); ​​​​ // x = bar::<u32>; ​​​​ baz(bar::<i32>); ​​​​ baz(bar::<u32>); ​​​​ quox(&mut bar::<u32>); ​​​​} ​​​​fn bar<T>() {} ​​​​fn baz(f: fn()) { ​​​​ println!("{}", std::mem::size_of_val(&f)); ​​​​} ​​​​fn quox<F>(f : &mut F) ​​​​ where F: FnMut(), ​​​​{ ​​​​ (f)() ​​​​}
  • 如果是 quox 函式使用到 FnMut bound,若傳入共享參考,你不能呼叫 f,因為你並沒有 f 的 mutable 參考 :
    ​​​​fn main() ​​​​{ ​​​​ ... ​​​​ quox(&bar::<u32>); ​​​​} ​​​​fn quox<F>(f : &F) ​​​​ where F: FnMut(), ​​​​{ ​​​​ (f)() ​​​​}
    目前程式碼
    ​​​​fn main() ​​​​{ ​​​​ println!("Hello, world!"); ​​​​ let mut x = bar::<i32>; ​​​​ println!("{}", std::mem::size_of_val(&x)); ​​​​ // x = bar::<u32>; ​​​​ baz(bar::<i32>); ​​​​ baz(bar::<u32>); ​​​​ quox(&bar::<u32>); ​​​​} ​​​​fn bar<T>() {} ​​​​fn baz(f: fn()) { ​​​​ println!("{}", std::mem::size_of_val(&f)); ​​​​} ​​​​fn quox<F>(f : &F) ​​​​ where F: FnMut(), ​​​​{ ​​​​ (f)() ​​​​}
    編譯會出現以下錯誤,因為傳入的並不是 mutable 參考,只是共享參考 :
    ​​​​$ cargo run ​​​​... ​​​​error[E0596]: cannot borrow `*f` as mutable, as it is behind a `&` reference ​​​​ --> src\main.rs:29:5 ​​​​ | ​​​​29 | (f)() ​​​​ | ^^^ `f` is a `&` reference, so the data it refers to cannot be borrowed as mutable ​​​​...
    如果你將 bound 改為 Fn 就可以呼叫 f :
    ​​​​fn main() ​​​​{ ​​​​ ... ​​​​ quox(&bar::<u32>); ​​​​} ​​​​fn quox<F>(f : &F) ​​​​ where F: Fn(), ​​​​{ ​​​​ (f)() ​​​​}
    目前程式碼
    ​​​​fn main() ​​​​{ ​​​​ println!("Hello, world!"); ​​​​ let mut x = bar::<i32>; ​​​​ println!("{}", std::mem::size_of_val(&x)); ​​​​ // x = bar::<u32>; ​​​​ baz(bar::<i32>); ​​​​ baz(bar::<u32>); ​​​​ quox(&bar::<u32>); ​​​​} ​​​​fn bar<T>() {} ​​​​fn baz(f: fn()) { ​​​​ println!("{}", std::mem::size_of_val(&f)); ​​​​} ​​​​fn quox<F>(f : &F) ​​​​ where F: Fn(), ​​​​{ ​​​​ (f)() ​​​​}

Closures

0:20:40

為什麼我們需要 Fn/ FnMut/ FnOnce 這些差異 ? 一旦你開始談論 closure,差異就會出現。

首先看到 non-capturing closure :

fn main() { let f = |x: i32, y:i32| x + y; }

closures 之所以被稱為 closure 是因為它們封閉了它們的環境。它們能夠從其環境中 capture 東西,並生成一個唯一的函式。這些函式在創建時會具體吸收、使用或參考周圍環境中的資料。這個例子,它不會從環境中 capture 任何內容。它僅參考自己的參數。因此這是一個 non-capturing closure。

non-capturing closure 的有趣之處在於它們可以強制轉換為函式指標,並且可以有三種 trait 的 bound :

fn main() { ... let f = || (); baz(f); quox(&f); } ... fn quox<F>(f : &F) where F: Fn(), { (f)() }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); bazbar::<u32>); quox(&bar::<u32>); let f = || (); baz(f); quox(&f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f : &F) where F: Fn(), { (f)() }

如果是 f 是 capturing closure 就不行 :

fn main() { ... let z = String::new(); // 用 not Copy 的型別比較好說明 let f = || { let _ = z ; // 耗用 z }; baz(f); quox(&f); }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let z = String::new(); let f = || { let _ = z ; }; baz(f); quox(&f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f : &F) where F: Fn(), { (f)() }

編譯產生以下錯誤 :

$ cargo run
...
error[E0308]: mismatched types
  --> src\main.rs:13:9
   |
10 |     let f = || {
   |             -- the found closure
...
13 |     baz(f);
   |     --- ^ expected fn pointer, found closure
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected fn pointer `fn()`
                 found closure `[closure@src\main.rs:10:13: 10:15]`
note: closures can only be coerced to `fn` types if they do not capture any variables
...

看到倒數第二行的說明,而我們程式的 f 真的有 capture 東西 (z)。

即便是只拿 z 的共享參考而不是移動 z,並將之列印出來也會產生相同編譯錯誤訊息 :

fn main() { ... let z = String::new(); let f = || { println!("{}", z); }; baz(f); quox(&f); }

f closure 仍會 capture 環境,這意味著你不能將 f closure 表示為函式指標。你可以將其想像成編譯器為 closure 產生了一種匿名結構,其中包含了從其環境中 capture 的所有欄位,當該 closure 執行時,它需要存取那些資訊

你可以把它想像成一個 f closure 結構,並且實作 Fn :

fn main() { ... let z = String::new(); let f = || { println!("{}", z); }; struct FClosure<'scope > { z: &'scope String, } impl<'scope> Fn() for FClosure<'scope> { fn call(&self) { // copy-paste from closure-definition println!("{}", self.z); } } baz(f); quox(&f); }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let z = String::new(); let f = || { println!("{}", z); }; // struct FClosure<'scope > // { // z: &'scope String, // } // impl<'scope> Fn() for FClosure<'scope> // { // fn call(&self) // { // // copy-paste from closure-definition // println!("{}", self.z); // } // } baz(f); quox(&f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f : &F) where F: Fn(), { (f)() }

你實際上無法編寫這段程式碼,但你可以將其視為編譯器產生的內容。因此你明白為什麼它需要存取 z 了。函式指標(只是指向此程式碼區塊的開頭的指標)是不夠的,它需要 z 的附加狀態

z 改成 mut :

fn main() { ... let mut z = String::new(); let f = || { z.clear(); }; // struct FClosure<'scope > // { // z: &'scope mut String, // 需要 exclusive 參考 // } // impl<'scope> Fn() for FClosure<'scope> // { // fn call(&self) // { // // copy-paste from closure-definition // self.z.clear(); // & &mut String -> &String // // 一個對字串的 exclusive 參考的共享參考, // // 只能用作字串的共享參考。 // } // } baz(f); quox(&f); }

Line 16 無法運作,因為 z 要求 mutable 參考,因為我們假設現在是 Fn,而 Fn 擁有的只是 &self

目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = || { z.clear(); }; // struct FClosure<'scope > // { // z: &'scope mut String, // } // impl<'scope> Fn() for FClosure<'scope> // { // fn call(&self) // { // // copy-paste from closure-definition // self.z.clear(); // } // } baz(f); quox(&f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f : &F) where F: Fn(), { (f)() }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
String::clear(&mut self)
Truncates this String, removing all contents.

While this means the String will have a length of zero, it does not touch its capacity.

如果現在是 FnMut,那我們得到的就會是 mutable 參考 :

fn main() { ... let mut z = String::new(); let f = || { z.clear(); }; // struct FClosure<'scope > // { // z: &'scope mut String, // 需要 exclusive 參考 // } // // 任何實作了 FnMut 的東西都實作了 FnOnce, // // 因此這裡也有實作 FnOnce。 // impl<'scope> FnMut() for FClosure<'scope> // { // fn call(&mut self) // { // // copy-paste from closure-definition // self.z.clear(); // } // } baz(f); // 編譯器提示錯誤 : // expected fn pointer, found closure // 並沒有強制轉換成函式指標, // 因為 `f` capture 了環境。 quox(&f); // 編譯器提示錯誤 : // expected a closure that implements the `Fn` trait, // but this closure only implements `FnMut` // f 無法實作 Fn,因為它需要對其環境進行 mutably borrow。 }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = || { z.clear(); }; // struct FClosure<'scope > // { // z: &'scope mut String, // } // impl<'scope> FnMut() for FClosure<'scope> // { // fn call(&mut self) // { // // copy-paste from closure-definition // self.z.clear(); // } // } baz(f); quox(&f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f : &F) where F: Fn(), { (f)() }

quox 函式的 bound 改成 FnMut() :

fn main() { ... let mut z = String::new(); let f = || { z.clear(); }; - baz(f); + // baz(f); // 為了說明 quox,先將這行註解掉 - quox(&f); + quox(f); } ... -fn quox<F>(f : &F) +fn quox<F>(mut f: F) - where F: Fn(), + where F: FnMut(), { (f)() }

由於 fFnMut,且 quox 要求的 bound 又是 FnMut,這次就可以編譯成功了。

目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = || { z.clear(); }; // struct FClosure<'scope > // { // z: &'scope mut String, // } // impl<'scope> FnMut() for FClosure<'scope> // { // fn call(&mut self) // { // // copy-paste from closure-definition // self.z.clear(); // } // } // baz(f) quox(f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(mut f: F) where F: FnMut(), { (f)() }

接著探討 FnOnce :

fn main() { ... let mut z = String::new(); let f = || { drop(z); // 為了卸除 z 我們必須有 z 的擁有權 // 所以我們需要將 z 移動到 closure 內。 // 你只能移動 z 到這個 closure 一次 }; // struct FClosure<'scope > // { // z: String // 需要擁有字串 // } // impl<'scope> FnMut() for FClosure<'scope> // { // fn call(&mut self) // { // // copy-paste from closure-definition // drop(self.z); // // 不可能在這裡卸除 z, // // 因為我們只有 z 的 mutable 參考。 // } // } // baz(f); quox(f); }

假設 closure 是 FnOnce 就沒有問題 (現在 closure 仍為 FnMut) :

fn main() { ... let mut z = String::new(); let f = || { drop(z); // 編譯器錯誤提示訊息 : // this closure implements `FnOnce`, not `FnMut` // // 為了卸除 z 我們必須有 z 的擁有權 // 所以我們需要將 z 移動到 closure 內。 // // 你只能移動 z 到這個 closure 一次 }; // struct FClosure<'scope > // { // z: String // 需要擁有字串 // } // impl<'scope> FnOnce() for FClosure<'scope> // { // fn call(self) // { // // copy-paste from closure-definition // drop(self.z); // // 可以在這裡卸除 z, // // 因為我們有 z 擁有權。 // } // } // baz(f); quox(f); }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = || { drop(z); }; // struct FClosure<'scope > // { // z: &'scope mut String, // } // impl<'scope> FnMut() for FClosure<'scope> // { // fn call(&mut self) // { // // copy-paste from closure-definition // self.z.clear(); // } // } // baz(f) quox(f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(mut f: F) where F: FnMut(), { (f)() }

使用 move 即可將值移入 closure :

fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); - let f = || { + let f = move || { println!("{}", z); + // z is dropped here }; // baz(f) quox(f); } ... -fn quox<F>(mut f: F) +fn quox<F>(f: F) - where F: FnMut(), + where F: FnOnce(), { (f)() }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = move || { drop(z); }; // baz(f) quox(f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f: F) where F: FnOnce(), { (f)() }

Non-static closures

0:33:49

如果你沒聲明 closure 的生命週期,編譯器預設它是 static :

fn make_fn() -> impl Fn() { let z = String::new(); || { println!("{}", z); // borrow z, // 無法編譯,因為 z 的生命週期是函式存活的時間。 } }

若你將 z 值移動到 closure 內,就可以編譯 :

fn make_fn() -> impl Fn() { let z = String::new(); move || { println!("{}", z); // 這個 closure 現在擁有 z, // 且 z 的生命週期跟 closure 一樣。 } }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = move || { drop(z); }; // baz(f) quox(f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(f: F) where F: FnOnce(), { (f)() } pub fn make_fn() -> impl Fn() { let z = String::new(); move || { println!("{}", z); } }

嘗試在 closure 裡卸除 z :

fn make_fn() -> impl Fn() { let z = String::new(); move || { drop(z); // 編譯器提示錯誤 : // move occurs because `z` has type `String`, // which does not implement the `Copy` trait // 因為回傳型態是 Fn 而不是 FnOnce。 } }

移動的一個缺點是會移動一切 closure 用到的東西 :

fn make_fn() -> impl Fn() { let x = String::new(); let z = String::new(); move || { println!("{}", x); println!("{}", z); } }

你不總是想要移動一切,避免移動一切的作法有 :

  • 能不要用 move 就不要用 :
    ​​​​fn make_fn() -> impl Fn() ​​​​{ ​​​​ let x = String::new(); ​​​​ let z = String::new(); ​​​​ || { ​​​​ println!("{}", x); ​​​​ println!("{}", z); ​​​​ } ​​​​}
  • 利用間接的方式,也就是僅傳入參考 :
    ​​​​fn make_fn() -> impl Fn() ​​​​{ ​​​​ let x = String::new(); ​​​​ let z = String::new(); ​​​​ let x2 = &x; ​​​​ // 這個例子不會運作,因為 x2 生命週期並非 static, ​​​​ // 但這裡想表達的是你可以使用這種方法來避免移動一切。 ​​​​ move || { ​​​​ println!("{}", x2); ​​​​ println!("{}", z); ​​​​ } ​​​​}
  • 如果你不在意舊值,可以使用 shadowing,引進新的 scope :
    ​​​​// 這個例子不會運作,原因 : ​​​​// 1. x 生命週期並非 static ​​​​// 2. 回傳型態為 () ​​​​// 但這裡想表達的是你可以使用這種方法來避免移動一切。 ​​​​fn make_fn() -> impl Fn() ​​​​{ ​​​​ let x = String::new(); ​​​​ let z = String::new(); ​​​​ { ​​​​ let x = &x; // 為了只移動參考 ​​​​ move || { ​​​​ println!("{}", x2); ​​​​ println!("{}", z); ​​​​ } ​​​​ } ​​​​}

Q : wait does that code create a new String in 'static, every time make_fn(I) is called?
A : 是的,如下 :

pub fn make_fn() -> impl Fn() { let z = String::new(); // 建立新的 String move || { println!("{}", z); // closure 擁有 z // 當 closure 被卸除時,z 才會被卸除。 } }

dyn Fn

0:38:50

接著看 trait 物件,Fn/ FnMut/ FnOnce 皆可被 dynamic dispatch :

  • dyn Fn()
    ​​​​fn hello(f: Box<dyn Fn()>) ​​​​{ ​​​​ f() ​​​​}
  • dyn FnMut()
    ​​​​fn hello(mut f: Box<dyn FnMut()>) ​​​​{ ​​​​ f() ​​​​}
  • dyn FnOnce()
    ​​​​fn hello(mut f: Box<dyn FnOnce()>) ​​​​{ ​​​​ f() ​​​​}

繼續看到 Box<dyn Fn_> :

// Box<dyn Fn()> did not implement Fn() // Box<dyn FnMut()> did not implement FnMut() // Box<dyn FnOnce()> did not implement FnOnce()

上述的三種情況的原因很有趣,假設我們想實作 Fn_ :

impl FnOnce() for Box<dyn FnOnce()> { fn call(self) { // self = Box, self.0 取 Box 裡面的值 let x: dyn FnOnce() = self.0; // x 的型別是 dyn FnOnce(), // 但 dyn FnOnce() 不是 Sized, // x 究竟占據 stack 記憶體多少空間 ? x.call() } }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Fn* closure traits implemented for Box<dyn Fn*>
This was ultimately due to a limitation in the compiler's ability to reason about such implementations, which has since been fixed with the introduction of unsized locals.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
0:42:42
繼續看到 Fn_ 實作 :

impl FnOnce() for Box<dyn FnOnce()> { fn call(self) // 你可能會問,為什麼不拿 Box 內容的共享參考 ? { // 如果你要呼叫 x, // 你必須擁有 self,你不能只拿到 self 的共享參考 let x: &dyn FnOnce() = &self.0; x.call() } }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Tracking issue for RFC #1909: Unsized Rvalues (unsized_locals, unsized_fn_params) #48055
有一個專門針對 Unsized Rvalues 的 RFC,這是此實作存在所必需的。RFC 已經完成,但還有很多實作問題,基本上,編譯器可以利用這個特定的功能,但這是不穩定的。你可以在 nightly 版本中選擇使用它,但通常不能在穩定版本上使用這個功能。但這對於這個特定的功能是必要的,這是一個有趣的小插曲。

當你擁有一個 Box<dyn Fn> 這樣的型別時,現在它是有效的,你不需要特別對待它,一般來說,dyn 就能正常工作。
:::

嘗試將 f 變成 &dyn Fn() 是可以的 :

fn main() { ... // baz(f) let f: &dyn Fn() = &f; // 這有點像函式指標,但它被允許有一個 self。 // 基本上它建構了一個用於這個 closure 的 cell, // 然後將其用作 dynamic dispatch 的資料指標, // 而 vtable 只有 call 方法。 quox(f); } ... fn quox<F>(mut f: F) where F: FnMut(), { (f)() }
目前程式碼
fn main() { println!("Hello, world!"); let mut x = bar::<i32>; println!("{}", std::mem::size_of_val(&x)); // x = bar::<u32>; baz(bar::<i32>); baz(bar::<u32>); quox(&bar::<u32>); let mut z = String::new(); let f = || { println!("{}", z); }; // baz(f) let f: &dyn Fn() = &f; quox(f); } fn bar<T>() {} fn baz(f: fn()) { println!("{}", std::mem::size_of_val(&f)); } fn quox<F>(mut f: F) where F: FnMut(), { (f)() } pub fn make_fn() -> impl Fn() { let z = String::new(); move || { println!("{}", z); } }

但若將 f 變成 &dyn FnMut() 就不可以,原因如下 :

fn main() { ... // baz(f) // 現在只有 f 的共享參考, // 所以 &dyn FnMut() 並未實作 FnMut,只實作 Fn。 let f: &dyn FnMut() = &f; quox(f); } ... fn quox<F>(mut f: F) where F: FnMut(), { (f)() }

當你使用 dynamic dispatch 的 Fn_ trait 時,你需要確保所使用的 wrapper、間接型別、寬指標型別允許你實際上需要的存取方式,以便呼叫該函式 :

let f: &mut dyn FnMut() = &f; let f: &dyn FnMut() = &f; let f: Box<dyn FnOnce()> = Box::new(f); // ok,因為 FnOnce 的限制比 FnMut 還嚴格。 // let f: Box<dyn FnMut()> = Box::new(f); // ok,因為 FnOnce 的限制比 Fn 還嚴格。 // let f: Box<dyn Fn()> = Box::new(f); // Arc 尚未支援 !!! // let f: std::sync::Arc<dyn Fn()> = std::sync::Arc::new(f); // Arc 允許你將 Unsized 的事物放入其中, // 但是出於必要,Arc 只會給你對內部事物的共享存取權限, // 因此 Arc<dyn Fn()> 實作了 Fn, // 但是 Arc<dyn FnMut()> 仍然只實作了 Fn,無法實作 FnMut。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Arc 尚未支援 dyn Fn() !!!
看到文件的 Implementors 部分 :

impl<Args, F, A> Fn<Args> for Box<F, A>

分析未支援 Arc 的原因 : Arc 支援 Unsized 的值。Arc 可以支持作為寬指標,因此它可以容納 dyn Fn(),如果它可以容納 dyn Fn(),並且能夠給你 closure state 的共享參考,那麼它應該能夠實作 Fn,因為這只需要能夠獲取 closure state 的共享參考。這表明這裡可能存在某種實作缺失,這可能是由於我們正在查看的關於 Unsized Rvalues 的問題有關,也許 Unsized Rvalues 被專門用來處理 Box

const Fn

0:49:44

接著看到 const Fn :

fn main() { let x = || 0; // constant closure // 你可以在編譯時期求值這個 closure foo(x); } const fn make_zero() -> i32 // 類似 Line 3 { 0 } const fn foo<F: FnOnce()>(f: F) { f() // 目前不行, // 因為編譯器不知道 f 可以當 constant 呼叫。 }

今天在穩定版本中,Line 15 的問題,目前無能為力。但有一個有趣的 pre-RFC 討論一直在進行,討論關於是否有一種方式可以說,我想要對任何實作此 trait 的型別進行泛型化,但僅使用可 constant 求值的事物。

Jon 目前想到的語法是 (1.61.0 之前可以編譯) :

+#![feature(const_trait_impl, const_fn_trait_bound)] fn main() { let x = || 0; foo(x); } const fn make_zero() -> i32 // 類似 Line 3 { 0 } -const fn foo<F: FnOnce()>(f: F) +const fn foo<F: ~const FnOnce()>(f: F) { f() }
  • const : FnOnce() 必須為 constant。
  • ~const : 如果 FnOnce() 是 constant,foo() 才會是 constant,反之非然。
  • ?const : 表 FnOnce() 可能是或不是 constant。
$ rustup override set nightly $ cargo run

比較主程式與非主程式傳入的 closure 皆為非 const 的情形 :

#![feature(const_trait_impl, const_fn_trait_bound)] fn main() { let x = || { String::from("new"); // x 是 !const -> foo 不是 const }; foo(x); // main 不是 const,所以沒要求 foo 是 const, // 所以 main 可以呼叫 foo。 } const fn test_foo() -> i32 { let x = || { String::from("new"); // x 是 !const -> foo 不是 const }; foo(x); // 在非 main 的地方呼叫 foo,要求 foo 是 const, // 所以 test_foo 不可以呼叫 foo。 } // ~const FnOnce() 在 Jon 測的當下,功能尚未穩定, // 所以 Line 14 ~ Line 24 Jon 沒有測出他想要展示的東西。 +const fn foo<F: ~const FnOnce()>(f: F) { f() }

for bounds

1:00:28

Q : Sometimes you end up with complicated lifetime bounds with for are.
A : 如下 :

fn main() { quox(|x| x); } fn quox<F>(f: F) where F: Fn(&str) -> &str { }

上面程式碼如預期運作,但有些情況下會變得更加複雜。比如在 bound 中,通常(雖然不總是)必須指定生命周期。在這裡,生命周期是什麼?通常情況下,當某物回傳時,你通常可以省略生命周期,但如果我們要嘗試指定這裡的生命周期,它們是什麼?因為這裡沒有生命周期。如果我們嘗試為這個 trait bound 填入生命周期,實際上是什麼呢? Line 5 沒有標記 'a,因為我們不能在這裡指派 'a。我們真正想說的是,我們希望 F 有生命周期,在輸出中重用其得到的生命周期。我們想說的是,它被允許參考與其輸入中參考的相同的東西。

特殊的 for 語法 :

fn main() { quox(|x| x); } fn quox<F>(f: F) where F: for<'a>Fn(&'a str) -> &'a str { }

F: for<'a>Fn(&'a str) -> &'a str 的實際意義是對 F: Fn(&str) -> &str 的 de-sugar。你可以把這個理解為對於任何生命週期標記 'aF 是一個從生命週期 'a&str 到另一個具有相同生命週期 'a&str 的函式實作,所以其實並不那麼複雜。

這是一種方式說明它需要對於任何生命週期都成立,這才是應該有的 bound 。實際上,你幾乎不需要給出這樣的 for bound,但有時如果你的 trait bound 具有生命週期但不是 Fn,則可能會發生這種情況。對於任何 FnFnMutFnOnce trait 的東西,編譯器通常很擅長推斷這一點,但一旦你在這裡開始擁有其他不是 Fn 的 traits,有時就需要使用 for,但這應該是非常非常罕見的。

closures in async fn

1:04:06

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
1:04:06
Q : If I want to pass a closure to async function the clousure needs to be 'static right? How does this kind of closure capture it's enviroment?
A : 如果你想將 closure 傳給 async fn,你可以這樣做,沒有什麼能阻止你接受任何函式 :

async fn quox<F>(f: F) where F: for<'a>Fn(&'a str) -> &'a str { }

它不需要是 static,更多的是通常與 Future 一起使用,特別是如果你想要像 tokio::spawn 這樣的東西 :

fn main() { tokio::spawn(quox(|x| x)); // 像 thread::spawn 一樣要求參數是 static } // 如果這裡的 F 不是 static, // 回傳的 Future 也不會是 static。 async fn quox<F>(f: F) where F: for<'a>Fn(&'a str) -> &'a str { } // de-sugar Line 9 - Line 14 // fn quox<F>(f: F) -> impl Future<Output = ()> // where // F: for<'a>Fn(&'a str) -> &'a str // { // async move { // let _ = f; // } // }

impl trait 就像 async fn 一樣,自動 capture 其輸入的生命週期。因此,如果輸入與某個生命週期相關聯,則輸出型別也將與該相同的生命週期相關聯,這意味著它將不是 static,除非輸入是 static。這就是為什麼你通常需要將 static 添加到傳遞給 async 函式的泛型中的原因,這不是因為它們是必需的,如以下例子 :

fn main() { quox(|x| x).await; // 如果在這裡 await,那麼回傳的 Future 不需要是 static。 // 只有在嘗試進行類似於 spawning 的操作時, // 才需要將 Future 的生命週期延長到當前的 stack frame 之外 }

Q : you often need to pin it, tho
A : 你應該很少需要手動 pin 東西,一般來說,await 語法應該就會處理它。

待整理

  1. 0:42:42
  2. 1:04:06