排序整数:
integers.sort();
闭包:即匿名函数表达式,可以用来排序复合类型:
struct City {
name: String,
population: i64,
country: String,
...
}
/// 按人口排序城市的辅助函数
// 接收City记录,然后提取键(Key)
fn city_population_descending(city: &City) -> i64 {
-city.population
}
// sort_by_key将键函数作为参数
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(city_population_descending);
}
// 上述两个函数通过闭包简写为如下所示
fn sort_cities(cities: &mut Vec<City>) {
cities.sort_by_key(|city| -city.population)
// |city| -city.population接收一个参数city,返回-city.polulation。
}
标准库中可接收闭包的特性:
Iterator
的 map
和 filter
方法,用于操作顺序数据thread::spawn
等线程 API。并发的核心是在线程间交换工作,而闭包可以方便地表示工作单元。HashMap
的 or_insert_with
方法。默认值以闭包形式传入,只会在必须创建新值时调用。闭包可以使用属于包含函数的数据:
/// 按几个不同的统计指标排序
fn sort_by_statisics(citeis: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(|city| -city.get_statistic(stat)); // 闭包捕获了stat
}
大多数支持闭包的语言,需要密切结合垃圾回收,如下面的 JavaScript 代码:
// 启动重排城市表格行的动画
function startSortingAnimation(cities, stat) {
// 用于排序表格的辅助函数
// 注意,这个函数引用了stat
function keyfn(city) {
return city.get_statistic(stat);
}
if (pendingSort)
pendingSort.cancel();
// 现在启动动画,传入keyfn
// 后面排序算法会调用keyfn
pendingSort = new SortingAnimation(cities, keyfn);
}
Rust 没有垃圾回收,如何实现这个特性?
闭包遵循借用和生命期规则。
在 12.1 的例子中,因为闭包包含对
stat
的引用,所以 Rust 不会让闭包的存活期超过
stat
。闭包只在排序期间使用。
stat
会被保存在栈上。Rust 使用生命期来确保代码安全,而不是垃圾回收。
如下例子:
use std::thread;
fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
-> thread::JoinHandle<Vec<City>>
// 闭包的返回值会包装在JoinHandle中返回给调用线程
{
let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };
thread::spawn(|| { // `||`表示闭包没有参数。
cities.sort_by_key(key_fn);
cities
})
}
thread::spawn
接收一个闭包,并在新的系统线程中调用这个闭包。新线程与调用程序并行运行。闭包返回后,新线程退出。
闭包 key_fn
包含对 stat
的引用。但此时 Rust 会拒绝这个程序,并提示以下信息:
问题1: `|city: &City| -> i64` // `stat` is borrowed here
问题2: (stat) // may outlive borrowed value `stat`
cities
属于不安全共享,thread::spawn
创建的新线程不能保证自己在 cities
和 stat
被销毁(函数结束)前完成任务
解决方案:让 Rust 把 cities
和 stat
转移到使用他们的闭包中,而不要再引用他们。
fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
-> thread::JoinHandle<Vec<City>>
{
let key_fn = move |city: &City| -> i64 { -city.get_statistic(stat) };
// key_fn获得了stat的所有权
thread::spawn(move || { // 获得了cities和key_fn的所有权
cities.sort_by_key(key_fn);
cities
})
}
在两个闭包前面加了 move
关键字:告诉 Rust,这个闭包不是在借用它使用的变量,而是要把它偷走。
Rust 为闭包提供了两种从包含函数取得数据的方式:转移和借用。—— 也是保证线程安全的方式
i32
),那么它会复制该值。因此,上述代码中 Statistic
恰好是一个可复制类型,那么在创建使用它的转移闭包后,同样还可以使用 stat
Vec
这样不可复制类型,才会真正转移。上述代码通过转移闭包,把 cities
转移到新线程中。在创建这个闭包的代码后面,Rust 不会再让其他人访问 cities
。cities
后,不需要再使用 cities
了。如果还需要使用,那么可以告诉 Rust 克隆 cities
并将副本保存到另一个变量中。闭包只能偷走它自己引用的那个副本。函数和闭包可以当成值来使用,自然它们也有自己的类型。
Rust 的函数值本质上就是指针:
下述函数的类型是 fn(&City) -> i64
:表示接收一个参数(&City
),返回一个 i64
值。
fn city_population_descending(city: &City) -> i64 {
-city.population
}
可以像使用其他值一样使用函数。即可以把函数保存在变量里,也可以使用常用的 Rust 语法计算函数值:
let my_key_fn: fn(&City) -> i64 =
if user.prefs.by_population {
city_population_descending
} else {
city_monster_attack_risk_descending
};
cities.sort_by_key(my_key_fn);
结构体可以包含函数类型的字段。
泛型类型如 Vec
可以存储一批函数,只要它们的 fn
类型一样就可以。
函数值很小,一个 fn
值就是这个函数机器码的内存地址,与 C++ 中的函数指针类似。
一个函数可以接收另一个函数作为参数:
/// 传入一组城市和一个测试函数
/// 返回满足条件的所有城市
fn count_selected_cities(cities: &Vec<City>,
test_fn: fn(&City) -> bool) -> usize
{
let mut count = 0;
for city in cities {
if test_fn(city) {
count += 1;
}
}
count
}
/// 测试函数举例。
/// 这个函数的类型是:fn(&City) -> bool
/// 跟count_selected_cities函数的参数test_fn的类型一致
fn has_monster_attacks(city: &city) -> bool {
city.monster_attack_risk > 0.0
}
// 计算城市被袭击的风险
let n = count_selected_cities(&my_cities, has_monster_attacks);
闭包与函数不是同一种类型,每个闭包都有自己的类型,在使用闭包的代码中通常需要是泛型的。
如下所示的例子:
let limit = preferences.acceptable_monster_risk();
let n = count_selected_cities(
&my_cities,
|city| city.monster_attack_risk > limit // 类型错误:类型不匹配
);
针对上述类型错误,必须修改函数的类型签名:
fn count_selected_cities<F>(cities: &Vec<City>, test_fn: F) -> usize
where F: Fn(&City) -> bool
{
let mut count = 0;
for city in cities {
if test_fn(city) {
count += 1;
}
}
count
}
新版本的 count_selected_cities
函数是一个泛型的,接收任意类型 F
的参数 test_fn
。F
必须实现特型 Fn(&City) -> bool
。所有以一个 &City
为参数且返回博而至的函数和闭包都会自动实现这个特型:
->
和后面的返回值类型是可选的。()
。fn(&City) -> bool // fn类型(仅函数)
Fn(&City) -> bool // Fn特型(包括函数和闭包)
新版本的 count_selected_cities
可以接收函数,也可以接收闭包:
count_selected_cities(
&my_cities,
has_moster_attacks
);
count_selected_cities(
&my_cities,
|city| city.monster_attack_risk > limit
);
虽然闭包可以调用,但它不是 fn
类型。闭包 |city| city.monster_attack_risk > limit
有自己的类型。这种类型通常是一个临时类型,大到足以存储它的数据。
任何两个闭包的类型都不相同。
Fn
特型。Fn(&City) -> bool
大多数编程语言的闭包通常是分配在堆上,动态分派,然后由垃圾回收程序负责回收。编译器很难对闭包进行行内化优化策略,以减少函数调用并进而引用其他优化。这样的闭包通常会拖慢内部循环(tight inner loop)的性能。
Rust 的闭包通常不会被分配在堆上,除非把闭包封装到 Box
、Vec
或其他容器里。每个闭包都有不同的类型,Rust 编译器只需要知道所调用闭包的类型,就可以将该闭包的代码行内化。所以,Rust 的闭包支持在内部循环中使用。
下面的例子中,闭包会引用两个局部变量:字符串 food
和值为 27 的简单枚举 weather
。
let food = "tacos";
let weather = Weather::Tornadoes;
|city| city.eats(food) && city.has(weather) // a、第一次使用闭包
move |city| city.eats(food) && city.has(weather) // b、第二次使用闭包
|city| city.eats("crawfish") // c、第三次使用闭包
闭包主要在创建的时候可能转移或借用被捕获的变量。所造成的影响十分不明显,特别是在闭包清除或修改捕获的值时。
杀值,即清除(drop)值,最直观的方式是调用 drop()
:
let my_str = "hello".to_string();
let f = || drop(my_str);
如果调用 f
两次:
f();
f();
第一次调用 f
,my_str
会被清除,意味着会释放存储字符串的内存,交还给系统。
第二次调用 f
,同样的操作又执行了一边。这就是 C++ 中会触发未定义行为的经典错误:重复释放(double free)。
而在 Rust 中,编译时检查可以发现上述错误。
一个闭包只能被调用一次。
闭包必须严格遵守生命期规则,即在调用时,值会被用尽(即转移)。
FnOnce
尝试欺骗 Rust,让它两次清除一个 String
。构造如下泛型函数:
fn call_twice(closure: F) where F: Fn() {
closure();
closure()
}
可以给这个函数传入任何实现 Fn
特型的闭包。这样的闭包不接收参数,且会返回 ()
。
把 12.4.1 中那个不安全闭包的闭包传入:
let my_str = "hello".to_string();
let f = || drop(my_str);
call_twice(f);
在编译时,Rust 会报以下错误:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
--> closures_twice.rs:12:13
|
12 | let f = || drop(my_str);
| ^^^^^^^^^^^^^^^
|
note: the requirement to implement `Fn` derives from here
--> closures_twice.rs:13:5
|
13 | call_twice(f);
| ^^^^^^^^^^
上述错误体现了 Rust 是如何处理 “杀值闭包” 的。即从语言层面上被完全禁止。
像 f
这样的闭包不能是 Fn
,而是不通用的特型 FnOnce
,这种特型的闭包只能调用一次。
第一次调用 FnOnce
闭包时,闭包本身也会被用掉。
如下所示,两个特型 Fn
和 FnOnce
的定义:
// 伪代码,没有参数
/// 对于`Fn`闭包,`closure()`方法会扩展为`closure().call()`,这个方法以自身的引用作为参数,因此这个闭包不会被转移。
trait Fn() -> R {
fn call(&self) -R;
}
/// 但是如果闭包只能安全地调用一次,即对于`FnOnce`闭包,`closure()`方法会被扩展为`closure().call_once()`,这个方法会取得`self`的值,所以闭包会被用掉。
trait FnOnce() -R {
fn call_once(self) -> R;
}
如下常见错误,在实际开发中需要注意:
直接迭代 dict
,导致它被用掉了。使得该闭包成了 FnOnce
类型。
let dict = produce_glossary();
let debug_dump_dict = || {
for (key, value) in dict {
println!("{:?} - {:?}", key, value);
}
};
应该改为 &dict
,而不是 dict
,即要访问值的引用。让这个闭包称为 Fn
类型。
let debug_dump_dict = || {
for (key, value) in &dict {
println!("{:?} - {:?}", key, value);
}
};
FnMut
Rust 认为非 mut
值可以安全地在线程间共享。
mut
闭包里包含了 mut
数据,同样是不安全的。FnMut
类型的闭包:包含可修改数据或 mut
引用的闭包。
即可以写数据的闭包。
FnMut
闭包要使用 mut
引用来调用。
定义:
// 特型定义为伪代码实现
/// Fn是没有调用次数限制的闭包和函数,是所有fn函数中最高的一种。
trait Fn() -> R {
fn call(&self) -R;
}
/// FnMut是如果闭包本身声明为mut,也可以多次调用的闭包。
trait FnMut() -R {
fn call_mut(&mut self) -R;
}
/// FnOnce是如果调用者拥有闭包,则只能调用一次。
trait FnOnce() -R {
fn call_once(self) -R;
}
任何需要以 mut
方式访问值,但不会清除任何值的闭包都是 FnMut
闭包。
let mut i = 0;
let incr = || { // incr是FnMut,而不是Fn
i += 1; // 借用对i的可修改引用
println!("Ding! i is now: {}", i);
};
call_twice(incr);
每个 Fn
都满足 FnMut
的要求,每个 FnMut
都满足 FnOnce
的要求。
Fn()
是 FnMut()
的子特型;FnMut()
是 FnOnce()
的子特型。call_twice 函数应该接收所有 FnMut 闭包,即应该修改为:
fn call_twice<F>(mut closure: F) where F: FnMut {
closure();
closure();
}
绑定由 F: Fn()
,修改为 F: FnMut()
。这样仍然可以接收所有 Fn
闭包。
此时可以对修改数据的闭包调用新的 call_twice
函数。
let mut i = 0;
call_twice(|| i += 1);
assert_eq!(i, 2);
回调:用户提供的函数,供库在以后调用。
以 Icon 框架举例:
type BoxedCallback = Box<Fn(&Request) -> Response>;
struct BasicROuter {
routes: HashMap<String, BoxedCallback>
}
每个箱子可以包含不通类型的闭包。
一个 HashMap
可以包含所有类型的回调。
调整相应的方法:
impl BasicRouter {
/// 创建一个空路由器
fn new() -> BasicRouter {
BasicRouter {
routes: HashMap::new()
}
}
/// 给路由器添加一个路由
fn add_route<C>(&mut self, url: &str, callback: C)
where C: Fn(&Request) -> Response + 'static
{
self.routes.insert(url.to_string(), Box::new(callback));
}
}
处理请求:
impl BasicRouter {
fn handle_request(&self, request: &Request) -> Response {
match self.routes.get(&request.url) {
None => not_found_response(),
Some(callback) => callback(request)
}
}
}
在 MVC(Model—View—Controller,模型 — 视图 — 控制器)设计模式中,对用户界面上的每个元素,MVC 框架都会创建 3 个对象:模型、视图和控制器。模型表示 UI 元素的状态;视图负责元素的外观;控制器处理用户交互。
在 Rust 中,必须明确所有权,必须消除循环引用。模型和控制器不能直接相互引用。
可以让每个闭包接收它需要的引用作为参数,通过闭包所有权和生命期来解决问题。
可以在系统中为每件东西分配一个数值,然后传递数值而不传递引用。
可以实现诸多 MVC 变体中的一种,保证对象之间并不是都相互引用。
可以仿效某个 非 MVC 系统,比如 Fackbook 的 Flux 架构,实现单向数据流。
from user input -> Action -> Dispatcher -> Store -> View -> to disblay
迭代器是闭包真正大显身手的主题。
详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十四章
原文地址