Rust 的 闭包(closures)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。我们将展示闭包的这些功能如何复用代码和自定义行为。
一、闭包的语法
可以从示例上看出,闭包的语法跟函数很像。三个部分如下:
|参数1, 参数2, ...|
->返回值类型
{执行体}
执行结果如下图:
可以发现没有报错正常通过了,即完成了我们预期的功能。
闭包的常见省略形式有如下三种:
而当闭包在捕获上文的自由变量时,管道符里的参数也可以省略:
如上图展示的代码,也是能正常通过编译的。
二、闭包的二次调用错误
示例代码如下:
可以看到首先我们有一个闭包的定义,当我们在调用闭包的时候,先传进去了string类型的参数,之后又调用闭包传进了int32类型的参数,此时编译就会出错了。
编译会出错的原因如下。第一次使用 String 值调用 example_closure 时,编译器推断 x 和此闭包返回值的类型为String 。接着这些类型被锁定进闭包 example_closure 中,如果尝试对同一闭包使用不同类型则会得到类型错误。
三、闭包捕获环境中的值
闭包可以作为内联匿名函数来使用,不过闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问其被定义的作用域的变量。
有示例代码如下所示:
这里,即便 x 并不是 equal_to_x 的一个参数, equal_to_x 闭包也被允许使用变量 x ,因为它与 equal_to_x 定义于相同的作用域。
这就是闭包的神奇之处,函数是不能做到这一点的。
当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销,在更一般的场景中,当我们不需要闭包来捕获环境时,我们不希望产生这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 Fn trait:
看完了rust文档中的描述,来做个简单总结:
Fn trait,表示闭包以不可变引用的方式来捕获环境中的自由变量,同时也表示该闭包没有改变环境的能力,并且可以多次调用。对应&self。
FnMut trait,表示闭包以可变引用的方式来捕获环境中的自由变量,同时意味着该闭包有改变环境的能力,也可以多次调用。对应&mut self。
FnOnce trait,表示闭包通过转移所有权来捕获环境中的自由变量,同时意味着该闭包没有改变环境的能力,只能调用一次,因为该闭包会消耗自身。对应self。
1、Fn闭包
有如下示例代码:
编译运行后,结果如下:
可以看到,闭包对环境中a的捕获,是以一种类似函数中不可变引用传参的方式进行的,闭包没有取得a的所有权,所以在闭包调用完之后,在println!中还可以正常使用a。
2、FnMut闭包
有如下示例代码:
当闭包中会修改外部环境的自由变量值时,函数闭包fn_mut_closure也得加上mut关键词。当闭包执行时,它默认以可变引用的形式传入闭包,闭包没有获取其所有权,所以闭包执行结束后,println!可以正常输出a的值。输出如下:
输出中可以看出,环境中的自由变量a的值确实被改变了。
3、FnOnce闭包
有示例如下:
这个示例中,闭包call_me中发生了所有权的移交(move语义),Box指针所指向的堆内存的所有权从a移交给了c,而随着c离开包后生命周期的结束,该堆内存也被释放掉。
环境中的自由变量a在被闭包执行一次之后,就失效了。
这种情况下二次调用肯定就发生报错了。
4、闭包强行获得环境中自由变量的所有权
如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 move 关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。
如我们在Fn闭包中展示的代码,如果我们在参数列表前加上了move关键字,如下图:
闭包获得了a的所有权,因此编译会出错:
编译出错的信息一目了然,就是因为move强行让所有权从a移到了闭包上,原来的a也就失效了,没有办法被println!打印出来。