1. 常規函數 函數都擁有顯示的類型簽名,其本身也是一種類型。 1.1 函數類型 自由函數 // 自由函數 fn sum(a: i32, b: i32) -> i32 { a+b } fn main() { assert_eq!(3, sum(1, 2)) } 關聯函數與方法 struct A(i3 ...
1. 常規函數
函數都擁有顯示的類型簽名,其本身也是一種類型。
1.1 函數類型
自由函數
// 自由函數
fn sum(a: i32, b: i32) -> i32 {
a+b
}
fn main() {
assert_eq!(3, sum(1, 2))
}
關聯函數與方法
struct A(i32, i32);
impl A {
// 關聯函數
fn sum(a: i32, b: i32) -> i32 {
a+b
}
// 方法: 第一個參數是self, &self或&mut self的函數
fn math(&self) -> i32 {
Self::sum(self.0, self.1)
}
}
fn main() {
let a = A(1, 2);
assert_eq!(3, A::sum(1, 2));
assert_eq!(3, a.math());
}
1.2 函數項類型
struct A(i32, i32);
impl A {
// 關聯函數
fn sum(a: i32, b: i32) -> i32 {
a+b
}
// 方法: 第一個參數是self, &self或&mut self的函數
fn math(&self) -> i32 {
Self::sum(self.0, self.1)
}
}
fn main() {
let a = A(1, 2);
let add = A::sum; // Fn item type
let add_math = A::math; // Fn item type
assert_eq!(add(1, 2), A::sum(1, 2));
assert_eq!(add_math(&a), a.math());
}
函數項類型是一個零大小的類型,會在類型中記錄函數的相關信息。
枚舉類型與元組結構體類型與函數項類型一樣,都是零大小類型。
enum Color {
R(i16),
G(i16),
B(i16),
}
// 等價於
// fn Color::R(_1: i16) -> Color { /* ... */}
// fn Color::G(_1: i16) -> Color { /* ... */}
// fn Color::B(_1: i16) -> Color { /* ... */}
fn main() {
println!("{:?}", std::mem::size_of_val(&Color::R)); // 0
}
這段代碼中Color::R
是一個類型構造體,等價於一個函數項。
Rust預設為函數項實現了一些trait:Copy
, Clone
, Sync
, Send
, Fn
, FnMut
, FnOnce
2. 函數指針
函數存放在記憶體的代碼區域內,它們同樣有地址。可以使用函數指針來指向要調用的函數的地址,將函數指針傳入函數中,就可以實現將函數本身作為函數的參數。
這樣傳遞函數的方式在C語言中非常常見。
type RGB = (i16, i16, i16);
fn color(c: &str) -> RGB {
(1, 1, 1)
}
// 這裡的參數類型fn(&str)->RGB是函數指針類型, fn pointer type
fn show(c: fn(&str)->RGB) {
println!("{:?}", c("black"));
}
fn main() {
let rgb = color; // rgb屬於函數項類型
show(rgb); // (1, 1, 1), 這裡發生了函數項類型到函數指針類型的隱式轉換
}
上述代碼中rgb
是一個函數項,屬於函數項類型(Fn item type),而show
函數的參數則是一個函數指針類型(Fn pointer type)。
fn main() {
let rgb = color; // 函數項類型
let c: fn(&str)->RGB = rgb; // 隱式轉換為了函數指針類型
println!("{:?}", std::mem::size_of_val(&rgb)); // 0
println!("{:?}", std::mem::size_of_val(&c)); // 8
}
應該儘可能使用函數項類型,這樣有助於享受零大小類型的優化。
3. 閉包
閉包可以捕獲環境變數,而函數則不可以。
// 以下代碼在Rust中會報錯
fn foo() -> fn(u32) {
let msg: String = "hello".to_string();
fn bar(n: u32) {
for _i in 0..n {
println!("{}", msg);
}
}
return bar;
}
fn main() {
let func = foo();
func(5);
}
以上代碼foo函數中定義的bar函數使用了環境變數msg
,然而在內部函數中使用這個環境變數是被編譯器所禁止的,編譯器會編譯報錯。這是因為Rust定義函數的語法無法指定如何捕獲環境變數,因此內部定義的函數無法在編譯時判斷使用的環境變數的生命周期是否合法,也因此Rust不允許在內部函數中使用環境變數。
想要實現以上功能就需要使用閉包。閉包在Rust中其實是一種語法糖,閉包的寫法如下所示
fn foo() -> impl Fn(u32) -> () {
let msg: String = "hello".to_string();
let bar = move |n: u32| -> () {
for _i in 0..n {
println!("{}", msg);
}
};
return bar;
}
fn main() {
let func = foo();
func(5);
}
bar
是一個完整的閉包定義,其中move
關鍵字表示捕獲的環境變數所有權會被轉義到閉包內,|n|
是閉包的參數,-> ()
表示返回值類型, {...}
內是閉包的具體代碼,Rust的閉包並不需要指定需要捕獲的變數,閉包中使用到的環境變數會被自動捕獲。Rust捕獲環境變數預設是獲取環境變數的引用,當使用了move
關鍵字時,則強制捕獲環境變數本身,這也就導致了所有權的轉移。
3.1 閉包語法糖
Rust的閉包,實際上是語法糖,它本質上是一個實現了特定trait的匿名的struct,與閉包相關的trait有這三個:
- Fn
- FnMut
- FnOnce
因此以上這種閉包代碼它可以被展開為如下代碼:
#![feature(unboxed_closures)]
fn foo() -> impl Fn(u32) -> () {
let msg: String = "hello".to_string();
struct ClosureEnvironment {
env_var: String,
}
impl FnOnce<(u32, )> for ClosureEnvironment {
type Output = ();
extern "rust-call" fn call_once(self, args: (u32, )) -> Self::Output {}
}
impl FnMut<(u32, )> for ClosureEnvironment {
extern "rust-call" fn call_mut(&mut self, args: (u32, )) -> Self::Output {}
}
impl Fn<(u32, )> for ClosureEnvironment {
extern "rust-call" fn call(&self, args: (u32, )) -> Self::Output {
let ClosureEnvironment { env_var } = self;
for _i in 0..args.0 {
println!("{}", env_var);
}
}
}
ClosureEnvironment { env_var: msg }
}
fn main() {
let func = foo();
func(5);
}
使用這個展開後的代碼,就可以理解閉包前的move
關鍵字的作用了,使用了move後,ClosureEnvironment
結構體中的環境變數env_var
保存的是String
對象本身,而非引用,向其中傳遞環境變數msg
,msg
的所有權就被轉移到了閉包內部。如果不使用move
關鍵字,Rust的閉包預設會將引用傳遞到閉包的結構體內,而不是轉移環境變數的所有權。
3.2 閉包的類型
閉包實現的trait可以為一下三種類型:
FnOnce類型
正如上面閉包展開代碼所示,實現FnOnce trait
中的call
方法時,第一個參數的類型是self
對象本身,這就會消耗閉包結構體,這也就是為什麼這種閉包只能調用一次。
編譯器把FnOnce的閉包類型看成函數指針。
FnMut類型
正如上面閉包展開代碼所示,實現FnMut trait
中的call
方法時,第一個參數的類型是&mut self
,是閉包對象的可變借用,不會消耗閉包結構體,切閉包函數可以對環境變數進行修改,可以被多次調用。
Fn類型
正如上面閉包展開代碼所示,實現Fn trait
中的call
方法時,第一個參數的類型是& self
,是閉包對象的不可變借用,不會消耗閉包結構體,閉包函數不可以對環境變數進行修改,可以被多次調用。
Fn: applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.
FnMut: applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.
FnOnce: applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits, because it can only be called once.
3.3 逃逸閉包與非逃逸閉包
如果使用閉包的作用域與定義閉包的作用域不同時,稱該閉包為逃逸閉包,否則為非逃逸閉包。
通常如果一個函數返回值為閉包類型,則該閉包就為逃逸閉包。
逃逸閉包會遇到一個問題:如果閉包捕獲了環境變數,閉包又離開了定義它的作用域,這時如果環境變數沒有move或者copy到閉包中,則會出現閉包引用了原作用域中已回收變數的問題。
因此如果需要將閉包作為函數的返回值時,需要使用move將環境變數的所有權轉移到閉包中,確保環境變數的生命周期在函數調用結束時不會結束。