Rust中的匿名函数与闭包(Closures)
根据Rust 0.6的tutorial整理。“闭包”这个概念来自于抽象代数,是指一个元素的集合在某个运算之下封闭,即将该运算应用于这个集合的元素,所得到的结果仍然是该集合的元素。不幸的是,Lisp社区将“闭包”用来表示另一种含义:闭包是一种为表示带有自由变量的过程而用的实现技术。简单说就是过程中可以使用自由变量(自由变量是相对于约束变量而言,约束变量即过程的形参,以及过程中定义的变量),严格地讲是指能捕获上下文环境。在编程语言中,“闭包”这个概念很不幸指的是Lisp社区的概念。
先使用“闭包”概念的源头——Lisp(Scheme)——来说明简单说明闭包。
我们定义一个函数make-add-n。该函数接受一个数值参数n,返回一个匿名函数。 该匿名函数接受一个数值参数i,返回i+n的值。
(define (make-add-n n)
(lambda (i)
(+ i n)))
lambda中的n对于lambda来讲就是自由变量。我们使用make-add-n生成一个add-3函数:
(define add-3 (make-add-n 3))
> (add-3 5)
8
可以看出,make-add-n的参数n在make-add-n调用结束后就已经离开了作用域,不存在了。但是add-3依然可以访问n(这里n=3),也就是说add-3(或者说make-add-n中的lambda)捕获了n(n=3)。
下面回到Rust。在Rust中,具名函数(用fn定义的那些给出了名字的函数)是不具备闭包能
力的,Rust中的闭包由匿名函数来完成。
1 Rust中的匿名函数
我们由一个例子引出匿名函数。下例中,我们定义一个匿名函数,该匿名函数接受一个参数,并将参数的值打印出来。然后我们将该匿名函数赋值给变量closure。let closure = |arg| io::println(fmt!("arg=%d", arg));然后closure就可以像函数一样调用:
closure(10);
从上例中可以看出,Rust的匿名函数由双竖线| |引出。双竖线中是匿名函数的参数列表,后面跟着匿名函数的表达式。匿名函数的参数类型以及返回值类型通常可以忽略,交由编译器推断。有时编译器推断不出来,则需要我们加上类型说明:
let square = |x: int| -> uint { x * x as uint };上面的例子中,匿名函数所做的操作都是很简单的,如果要做一点复杂的事情怎么办?比如要用if判断?用下面这个例子说明:
let abs = |x: int| {
if x > 0 {
x
} else { -x }
};
到目前为止一切似乎都很简单,哦,等等,复杂的来了——“闭包”。
闭包本身不复杂,那个scheme的例子基本说明了闭包是怎么一回事。不过呢,到了Rust中,事情变得有趣了很多。我们还记得Rust的内存模型吧?Rust中的数据有三处存储地可选:栈上、托管堆上、以及交换堆上。所以呢,Rust中有三种闭包,你没听错,是三种。分别对应于三种存储地:stack closure、managed closure、owned closure。
注:tutorial中是把closure和匿名函数合在一起讲的,重点在强调closure特性,但我觉得分开说好一些,匿名函数可以使用闭包,也可以不使用闭包。不使用闭包特性的匿名函数照样很有用。使用stack closure、managed closure、owned closure术语是重点强调这几种匿名函数对于捕获的变量的要求,所以我也沿用这几个术语。
2 Stack closures
Stack closures的类型为&fn,可以直接访问它上下文环境中的局部变量:let mut max = 0; [1, 2, 3].map(|x| if *x > max { max = *x });Stack closures非常高效,因为它们的环境是分配在栈上,同时通过指针访问捕获的变量(不用拷贝)。为了保证stack closures的生命周期不会比它们捕获的变量还长,stack closures不能作为第一等级函数使用。就是说:它们不能存储在数据结构中,也不能当作函数的返回值;它们通常用作某些高阶函数的参数,如上例中作为map的参数。除了这些限制,stack closure在Rust中被普遍使用(匿名简短小函数)。
3 Managed closures
Managed closures对应于managed boxes,即在托管堆上存储的closures。因此生存周期没有stack closures那样的限制。类型为@fn。是第一等级函数,可以存储,可以被当作返回值返回。当然,继承自managed boxes的限制,managed closures无法跨越tasks边界。另外,managed closure不直接访问它的上下文环境,而是将它捕获的值拷贝为私有数据结构。因此它不能对这些捕获的变量进行赋值,也无法检测到外部值得变化。
来看一个例子:
fn mk_appender(suffix: ~str) -> @fn(~str) -> ~str { // The compiler knows that we intend this closure to be of type @fn return |s| s + suffix; } fn main() { let shout = mk_appender(~"!"); io::println(shout(~"hey ho, let's go")); }
4 Owned closures
Owned closures,写作~fn,类似于owned boxes,存储于交换堆,因此可以在tasks之间传递。和managed boxes类似,也拷贝它们所捕获的值,不同的是,owned closures拥有这些值的所有权,别的代码无法访问这些捕获的值。Owned closures用于并发代码,特别是生成tasks。5 匿名函数的通用性
Rust的匿名函数有个很便利的子类型化特性:你可以将任何类型的匿名函数(只要参数和返回值类型匹配)传递给接受fn()为参数的函数。因此,如果写了一个高阶函数,仅仅调用其的函数参数,而不对参数作别的事,那么应该将其参数声明为fn()类型。这样的话,调用者就可以传入任意类型的函数(包括具名函数)。fn call_twice(f: fn()) { f(); f(); } let closure = || { "I'm a closure, and it doesn't matter what type I am"; }; fn function() { "I'm a normal function"; } call_twice(closure); call_twice(function);
6 Do 记法
do记法提供了将高阶函数(以函数为参数的函数)转换为控制结构的方法。例如,each函数对一个vector的元素进行迭代,以一个vector和一个函数为参数,将函数应用于vector的每个元素上:
fn each(v: &[int], op: fn(v: &int)) { let mut n = 0; while n < v.len() { op(&v[n]); n += 1; } }调用each时,如果我们使用匿名函数作为参数,那么我们可以写成一种很好看的形式:
each([1, 2, 3], |n| { do_some_work(n); });这种形式很常用,因此Rust提供了一个特殊的函数调用形式,使得上面的调用看起来更像内置的控制结构:
do each([1, 2, 3]) |n| { do_some_work(n); }上面的方式以关键字do开头,不将最后的匿名函数参数写入括号内,而是写在括号外,这样看起来更像一个常见的代码块(很像Ruby的Block)。
使用do来调用task::spawn创建task很方便。spawn函数的类型签名为spawn(fn: ~fn())。也就是说,spawn是一个函数,接受一个无参数的ownend closure为参数。
use task::spawn; do spawn() || { debug!("I'm a task, whatever"); }上面的括号和竖线中什么都没有,所以可以省略:
do spawn { debug!("Kablam!"); }
7 For 循环
Rust中最常用的进行迭代的方式是使用for循环。和do类似,for也有着将closures描述成控制结构的漂亮语法。另外,在for循环中,break、loop、以及return的作用和它们在while和loop中相同。还是用each函数这个例子,这次我们当函数参数返回false时break:
fn each(v: &[int], op: fn(v: &int) -> bool) { let mut n = 0; while n < v.len() { if !op(&v[n]) { break; } n += 1; } }使用该函数对vector进行迭代:
each([2, 4, 8, 5, 16], |n| { if *n % 2 != 0 { println("found odd number!"); false } else { true } });有了for,each这样的函数就可以更像内建循环结构。在for循环中调用each,不需要通过返回false来跳出循环,直接使用break即可。如果需要调制下一次迭代开头,使用loop。
for each([2, 4, 8, 5, 16]) |n| { if *n % 2 != 0 { println("found odd number!"); break; } }另外,你还可以在作为for循环体的block中使用return,这在closures中通常是不允许的,这时return将从使用for的函数中返回,而不仅仅是循环体。
fn contains(v: &[int], elt: int) -> bool { for each(v) |x| { if (*x == elt) { return true; } } false }注意,each将每个值通过borrowed pointer传递,所以每次使用值时需要解引用。我们可以使用Rust的参数模式匹配来直接获取值,而不是指针:
for each(v) |&x| { if (x == elt) { return true; } }for记法只能用于stack closures。
没有评论:
发表评论