引子
Rust对函数式编程有着非常良好的支持,从闭包这一特性就能看出来,它不仅实现了经典的功能和语义,还派生出Fn
, FnOnce
, FnMut
这几个trait帮助我们处理变量的所有权和引用的问题。
然而在这里,要重述一个事实,以防读者把在学习其他语言时产生的偏见带入Rust:闭包不等于匿名函数,它的正式定义为
Operationally, a closure is a record storing a function together with an environment. ...
即闭包等于: “匿名”函数 + 从闭包外部(还是在当前作用域内)捕捉的变量,连同整个作用域一起,称之为环境(environment)。简单一点说,就是延伸了作用域的函数,其中包含了在函数主体中使用却未在函数签名中定义的变量。而函数是否匿名根本无关紧要。
Rust的闭包的完整语法格式为:|param: type| -> return type { func body }
其中||
为闭包接受参数的地方,如果你调用了该闭包,得益于Rust的类型推导,参数类型和返回类型可以省略,否则不可省略。当函数主体只有一句时,可以省略那对花括号。
比如,一个简单的闭包:返回一个它接受的值:
let origin = |z| z;
let eight = origin(8);// eight: 8
let text = origin("text"); // compile error
值得注意的是,只要闭包的类型确定(不管是在类型推导后确定的还是最初就定义好的),就不能改变。
在深入讲解Rust的闭包之前,我们先看看在有垃圾回收(garbage collection)的语言中,闭包是什么样的。
# Python3: 一个计算平均值的函数
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
函数捕获了变量cnt
和total
,此外,在函数cal_avg
内部,还定义了一个局部变量useless_value
,这是为了说明普通的局部变量和被闭包引用的变量有什么不同,我们知道,一旦一个对象的引用计数为0时,它就不能再被获取(弱引用除外),就会被当作垃圾而回收掉以释放资源,很显然,在一个函数被调用完成后,除了返回值,其他所有局部变量都不可获取,但是闭包引用的值仍然没有被回收,这是因为,变量avg
引用了函数值cal_avg()
即内部的average
函数,所以变量total
和cnt
一直都在被引用,这才没有被当作垃圾回收。
Rust没有垃圾回收,那是如何设计的呢?
借用
假设现在有一个描述城市的City
结构体,包含这座城市的名字,人口数量,所在国家等等信息,那么它的定义应该如下:
struct City {
name: String,
country: String,
population: f64,
...
}
假设我们现在有几个类型为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];
}
我们想按照城市的人口数量的由多到少来为它进行排序,那么可以这么写:
fn sort_cities(cities: &mut Vec, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat)); // borrow a shared reference to stat
println("{:?}", stat); // still fine
}
注意,闭包在这里使用了stat
这个变量,而这个变量是在外部的函数中定义的。我们说闭包的这一行为是:捕捉变量。在这种情况下,它自动借用了stat
的引用,理由很简单:因为闭包捕获了这个值,所以必然存在对它的引用。
移动
我们还可以把变量的所有权转移到闭包中,为此,使用关键字move
:
fn sort_cities(cities: &mut Vec, 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
}
当然,如果闭包中捕获的变量实现了Copy
的trait
,闭包会复制它而不是移动。
简而言之,Rust使用了生命周期而不是垃圾回收保证了安全性,然而,Rust的方法却快的多:甚至是快速垃圾回收都要比在存储在栈上的stat
这种情况要慢一些,而Rust正是这样做的。
函数和闭包类型
函数和闭包都有各自的类型,举例来说:
fn insertion_sort(param: &mut Vec) {
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)) {
unimplemented!();
}
上面这个函数的类型是:fn(&mut Vec
,而我们描述闭包的类型时,是使用Fn
, FnOnce
和FnMut
这几个trait去描述它们的
但是,take_closure
这个函数也可以接受一个函数作为参数,而fn()->()
这种形式只适用于函数。
Fn
, FnOnce
, FnMut
- 当一个闭包中只有对捕获变量的不可变引用时,我们说它实现了
Fn
这个trait。 - 当一个闭包中发生了变量所有权的移动或者是某些值被消耗掉,
drop
, 我们说它实现的是FnOnce
这个trait,即这个闭包只能使用一次。所有的函数和闭包都默认实现了这一trait。 - 当一个闭包中出现了对变量的可变引用时,我们说它实现了
FnMut
这个trait。
它们三者之间的关系可以用集合论的方法来认识,FnOnce
包含FnMut
包含Fn
。