fn 與 Fn
Wu Yu Wei published on
3 min,
590 words
大家在其他語言中大部分的時候可能可以將 fn
與 Fn
視為同樣的語意來處理,可是這在 Rust 中會需要多一些操作,比方說以下的做法就是不可行的:
fn foo<A, B>(f1: Fn(A) -> B, f2: Fn(B) -> A) {
todo!()
}
因為 fn
才是函式(更確切地說是函式指標),而 Fn
是 trait,我們得改成用 fn
或是真的要用 Fn
的話則是採用泛型 impl trait
甚至是 Trait Object &dyn trait
等等。選擇 impl trait
的話會寫成像這樣:
fn foo<A, B>(f1: impl Fn(A) -> B, f2: impl Fn(B) -> A) {
todo!()
}
會有這樣的差別的原因是,除了普通的函式以外,閉包也擁有 Fn
trait,閉包通常會再有個 context struct 來保存狀態,在大多數的語言中會選擇 Box 起來變成同樣的指標,讓型別之間的區別抹去。在 Rust 預設則不是如此,型別之間的區別是存在的,所以真的想在執行時視為同樣的型別處理,我們就會用 Trait Object Box<dyn trait>
,但這樣就是動態調度(Dynamic Dispatch),和其他語言一樣會有額外的開銷。
Fn
預設的行為則不然,它是 trait,用在泛型上會是靜態調度(Static Dispatch),在執行時是沒有任何額外的開銷(不過因為是靜態的,你的程式碼大小的確會增加)。有趣的是 fn
因為是指標,所以拿來當作參數使用的話其實是動態調度的,和使用 Fn
的泛型靜態調度有一點不一樣。詳細的區別可以看看以下例子:https://godbolt.org/z/bexE6v
#![feature(test)]
#[inline(never)]
fn foo() {
std::hint::black_box(());
}
#[inline(never)]
fn bar() {
std::hint::black_box(());
std::hint::black_box(());
}
pub fn run1() {
#[inline(never)]
fn run(f: fn()) { f() }
run(foo);
run(bar);
}
pub fn run2() {
#[inline(never)]
fn run(f: impl Fn()) { f() }
run(foo);
run(bar);
}
而產生的組合語言則是如此:
example::foo:
push rax
mov rax, rsp
pop rax
ret
example::bar:
push rax
mov rax, rsp
pop rax
ret
example::run1:
push rax
lea rdi, [rip + example::foo]
call example::run1::run
lea rdi, [rip + example::bar]
pop rax
jmp example::run1::run
example::run1::run:
jmp rdi
example::run2:
push rax
call example::run2::run
pop rax
jmp example::run2::run
example::run2::run:
jmp example::foo
example::run2::run:
jmp example::bar
run1
會接受的參數是個指標,而 run2
才會確切呼叫實際個別函式。