Rust中的闭包:更快更安全

引子

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函数捕获了变量cnttotal,此外,在函数cal_avg内部,还定义了一个局部变量useless_value,这是为了说明普通的局部变量和被闭包引用的变量有什么不同,我们知道,一旦一个对象的引用计数为0时,它就不能再被获取(弱引用除外),就会被当作垃圾而回收掉以释放资源,很显然,在一个函数被调用完成后,除了返回值,其他所有局部变量都不可获取,但是闭包引用的值仍然没有被回收,这是因为,变量avg引用了函数值cal_avg()即内部的average函数,所以变量totalcnt一直都在被引用,这才没有被当作垃圾回收。

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
}

当然,如果闭包中捕获的变量实现了Copytrait,闭包会复制它而不是移动。

简而言之,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, FnOnceFnMut这几个trait去描述它们的

但是,take_closure这个函数也可以接受一个函数作为参数,而fn()->()这种形式只适用于函数。

Fn, FnOnce, FnMut

  • 当一个闭包中只有对捕获变量的不可变引用时,我们说它实现了Fn这个trait。
  • 当一个闭包中发生了变量所有权的移动或者是某些值被消耗掉,drop, 我们说它实现的是FnOnce这个trait,即这个闭包只能使用一次。所有的函数和闭包都默认实现了这一trait。
  • 当一个闭包中出现了对变量的可变引用时,我们说它实现了FnMut这个trait。

它们三者之间的关系可以用集合论的方法来认识,FnOnce包含FnMut包含Fn

你可能感兴趣的:(Rust中的闭包:更快更安全)