引子
Rust對函數(shù)式編程有著非常良好的支持,從閉包這一特性就能看出來歉糜,它不僅實現(xiàn)了經(jīng)典的功能和語義,還派生出Fn
, FnOnce
, FnMut
這幾個trait幫助我們處理變量的所有權(quán)和引用的問題望众。
然而在這里匪补,要重述一個事實,以防讀者把在學(xué)習(xí)其他語言時產(chǎn)生的偏見帶入Rust:閉包不等于匿名函數(shù)烂翰,它的正式定義為
Operationally, a closure is a record storing a function together with an environment. ...
即閉包等于: “匿名”函數(shù) + 從閉包外部(還是在當前作用域內(nèi))捕捉的變量夯缺,連同整個作用域一起,稱之為環(huán)境(environment)甘耿。簡單一點說踊兜,就是延伸了作用域的函數(shù),其中包含了在函數(shù)主體中使用卻未在函數(shù)簽名中定義的變量佳恬。而函數(shù)是否匿名根本無關(guān)緊要捏境。
Rust的閉包的完整語法格式為:|param: type| -> return type { func body }
其中||
為閉包接受參數(shù)的地方,如果你調(diào)用了該閉包毁葱,得益于Rust的類型推導(dǎo)垫言,參數(shù)類型和返回類型可以省略,否則不可省略倾剿。當函數(shù)主體只有一句時筷频,可以省略那對花括號。
比如,一個簡單的閉包:返回一個它接受的值:
let origin = |z| z;
let eight = origin(8);// eight: 8
let text = origin("text"); // compile error
值得注意的是截驮,只要閉包的類型確定(不管是在類型推導(dǎo)后確定的還是最初就定義好的)笑陈,就不能改變。
在深入講解Rust的閉包之前葵袭,我們先看看在有垃圾回收(garbage collection)的語言中涵妥,閉包是什么樣的。
# Python3: 一個計算平均值的函數(shù)
def cal_avg():
useless_value = "I'm useless"
cnt = 0 #----------closure start----------#
total = 0
def average(new_value):
nonlocal cnt, total
cnt += 1
total += new_value
return total/cnt #----------closure end----------#
return average
def main():
avg = cal_avg()
print(avg(10)) // 10.0
avg(5) // 7.5
print(avg(3)) // 6.0
if __name__ == "__main__":
main()
在Python中坡锡,我們使用nonlocal
來聲明捕獲的變量蓬网,否則就當作是局部變量使用。在這個例子中鹉勒,average
函數(shù)捕獲了變量cnt
和total
帆锋,此外,在函數(shù)cal_avg
內(nèi)部禽额,還定義了一個局部變量useless_value
锯厢,這是為了說明普通的局部變量和被閉包引用的變量有什么不同,我們知道脯倒,一旦一個對象的引用計數(shù)為0時实辑,它就不能再被獲取(弱引用除外),就會被當作垃圾而回收掉以釋放資源藻丢,很顯然剪撬,在一個函數(shù)被調(diào)用完成后,除了返回值悠反,其他所有局部變量都不可獲取残黑,但是閉包引用的值仍然沒有被回收,這是因為斋否,變量avg
引用了函數(shù)值cal_avg()
即內(nèi)部的average
函數(shù)梨水,所以變量total
和cnt
一直都在被引用,這才沒有被當作垃圾回收如叼。
Rust沒有垃圾回收冰木,那是如何設(shè)計的呢?
借用
假設(shè)現(xiàn)在有一個描述城市的City
結(jié)構(gòu)體笼恰,包含這座城市的名字踊沸,人口數(shù)量,所在國家等等信息社证,那么它的定義應(yīng)該如下:
struct City {
name: String,
country: String,
population: f64,
...
}
假設(shè)我們現(xiàn)在有幾個類型為City
的值:new_york
, seattle
, london
,組成的向量表:
const new_york = City {
name: "New York".to_string(),
country: "USA".to_string(),
population: 851e4,
}
const seattle = City {
name: "Seattle".to_string(),
country: "USA".to_string(),
population: 478e4,
}
const london = City {
name: "London".to_string(),
country: "UK".to_string(),
population: 890e4,
}
fn main() {
let mut city_list = vec![new_york, seattle, london];
}
我們想按照城市的人口數(shù)量的由多到少來為它進行排序逼龟,那么可以這么寫:
fn sort_cities(cities: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat)); // borrow a shared reference to stat
println("{:?}", stat); // still fine
}
注意,閉包在這里使用了stat
這個變量追葡,而這個變量是在外部的函數(shù)中定義的腺律。我們說閉包的這一行為是:捕捉變量奕短。在這種情況下,它自動借用了stat
的引用匀钧,理由很簡單:因為閉包捕獲了這個值翎碑,所以必然存在對它的引用。
移動
我們還可以把變量的所有權(quán)轉(zhuǎn)移到閉包中之斯,為此日杈,使用關(guān)鍵字move
:
fn sort_cities(cities: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(move |city| -city.get_statistic(stat)); // move the ownership of stat to the closure
println!("{:?}", stat);// compile error! used moved value `stat`
// If Statistic implements Copy, then stat is still avaiable
println!("{:?}", stat);// It's fine
}
當然,如果閉包中捕獲的變量實現(xiàn)了Copy
的trait
佑刷,閉包會復(fù)制它而不是移動莉擒。
簡而言之,Rust使用了生命周期而不是垃圾回收保證了安全性瘫絮,然而涨冀,Rust的方法卻快的多:甚至是快速垃圾回收都要比在存儲在棧上的stat
這種情況要慢一些,而Rust正是這樣做的麦萤。
函數(shù)和閉包類型
函數(shù)和閉包都有各自的類型鹿鳖,舉例來說:
fn insertion_sort(param: &mut Vec<i32>) {
unimplemented!()
}
let num = vec![1, 2, 3];
let print_num_vector = |param| {
for i in param {
println!("{}", i);
}
};
print_num_vector(&num);
fn take_closure(closure: impl Fn(&Vec<i32>)) {
unimplemented!();
}
上面這個函數(shù)的類型是:fn(&mut Vec<i32>)
,而我們描述閉包的類型時壮莹,是使用Fn
, FnOnce
和FnMut
這幾個trait去描述它們的
但是栓辜,take_closure
這個函數(shù)也可以接受一個函數(shù)作為參數(shù),而fn()->()
這種形式只適用于函數(shù)垛孔。
Fn
, FnOnce
, FnMut
- 當一個閉包中只有對捕獲變量的不可變引用時,我們說它實現(xiàn)了
Fn
這個trait施敢。 - 當一個閉包中發(fā)生了變量所有權(quán)的移動或者是某些值被消耗掉周荐,
drop
, 我們說它實現(xiàn)的是FnOnce
這個trait,即這個閉包只能使用一次僵娃。所有的函數(shù)和閉包都默認實現(xiàn)了這一trait概作。 - 當一個閉包中出現(xiàn)了對變量的可變引用時,我們說它實現(xiàn)了
FnMut
這個trait默怨。
它們?nèi)咧g的關(guān)系可以用集合論的方法來認識讯榕,FnOnce
包含FnMut
包含Fn
。