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
wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> rustc --version
rustc 1.70.0 (90c541806 2023-05-31) (built from a source tarball)
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.
Jon 寫了 Rust for Rustaceans,讀這本書之前必須先有先備知識,至少要看過 The Rust Programming Language。
開始建置 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 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
函式,即使我們將具 T
的 bar
函式 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));
}
接著看到 Fn()
:
fn main()
{
...
quox(bar::<u32>);
}
fn bar<T>() {}
...
fn quox<F>(f : F)
where F: Fn(),
{
}
Fn()
與 fn()
是不一樣的東西,雖然它們的建構方式相同。fn()
作為函式指標使用,Fn()
則是 trait。
pub trait Fn<Args>: FnMut<Args>
where
Args: Tuple,
{
// Required method
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
// 傳入 self 的參考
}
Fn()
在某種程度上相當於擁有共享參考,因為你可以多次呼叫它,並且你可以透過共享參考同時多次呼叫它,或者至少是透過一個共享參考。
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。
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 的一種思考方式是,它們是一種階層結構 :
對應文件 :
Since both
Fn
andFnMut
are subtraits ofFnOnce
, any instance ofFn
orFnMut
can be used where aFnOnce
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
其中一個。
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)()
}
$ 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
...
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)()
}
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)()
}
$ 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
...
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)()
}
為什麼我們需要 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)()
}
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)()
}
由於 f
為 FnMut
,且 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)()
}
如果你沒聲明 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);
}
}
// 這個例子不會運作,原因 :
// 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 才會被卸除。
}
}
接著看 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()
}
}
Fn_
實作 :
impl FnOnce() for Box<dyn FnOnce()>
{
fn call(self)
// 你可能會問,為什麼不拿 Box 內容的共享參考 ?
{
// 如果你要呼叫 x,
// 你必須擁有 self,你不能只拿到 self 的共享參考
let x: &dyn FnOnce() = &self.0;
x.call()
}
}
當你擁有一個 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。
dyn Fn()
!!!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
:
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()
}
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。你可以把這個理解為對於任何生命週期標記 'a
,F
是一個從生命週期 'a
的 &str
到另一個具有相同生命週期 'a
的 &str
的函式實作,所以其實並不那麼複雜。
這是一種方式說明它需要對於任何生命週期都成立,這才是應該有的 bound 。實際上,你幾乎不需要給出這樣的 for
bound,但有時如果你的 trait bound 具有生命週期但不是 Fn
,則可能會發生這種情況。對於任何 Fn
、FnMut
或 FnOnce
trait 的東西,編譯器通常很擅長推斷這一點,但一旦你在這裡開始擁有其他不是 Fn
的 traits,有時就需要使用 for
,但這應該是非常非常罕見的。
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
語法應該就會處理它。