12 - 闭包
-
排序整数:
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
方法。默认值以闭包形式传入,只会在必须创建新值时调用。
12.1 - 捕获变量
-
闭包可以使用属于包含函数的数据:
/// 按几个不同的统计指标排序 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.1 - 借用值的闭包
-
闭包遵循借用和生命期规则。
-
在 12.1 的例子中,因为闭包包含对
stat
的引用,所以 Rust 不会让闭包的存活期超过
stat
。闭包只在排序期间使用。
- 此处
stat
会被保存在栈上。 - 相对 GC 分配会比较快。
- 此处
-
Rust 使用生命期来确保代码安全,而不是垃圾回收。
12.1.2 - 盗用值的闭包
-
如下例子:
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<City>
这样不可复制类型,才会真正转移。上述代码通过转移闭包,把cities
转移到新线程中。在创建这个闭包的代码后面,Rust 不会再让其他人访问cities
。- 上述代码在闭包转移
cities
后,不需要再使用cities
了。如果还需要使用,那么可以告诉 Rust 克隆cities
并将副本保存到另一个变量中。闭包只能偷走它自己引用的那个副本。
- 如果闭包转移了一个可复制的值(
12.2 - 函数与闭包类型
-
函数和闭包可以当成值来使用,自然它们也有自己的类型。
-
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
- 所有的闭包都会实现
12.3 - 闭包的性能
-
大多数编程语言的闭包通常是分配在堆上,动态分派,然后由垃圾回收程序负责回收。编译器很难对闭包进行行内化优化策略,以减少函数调用并进而引用其他优化。这样的闭包通常会拖慢内部循环(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、第三次使用闭包
- 闭包 a、在内存中,这个闭包类似一个小结构体,其包含对他所引用变量的引用。
- 闭包 b、与上相同,不过它是一个转移闭包,因此会包含实际的值,而非引用。
- 闭包 c、没有用到起环境中的任何变量。此时结构体是空的,因此这个闭包根本不会占用内存。
12.4 - 闭包和安全 —— 在堆上分配闭包的方法
闭包主要在创建的时候可能转移或借用被捕获的变量。所造成的影响十分不明显,特别是在闭包清除或修改捕获的值时。
12.4.1 - 杀值的闭包
-
杀值,即清除(drop)值,最直观的方式是调用
drop()
:let my_str = "hello".to_string(); let f = || drop(my_str);
-
如果调用
f
两次:f(); f();
-
第一次调用
f
,my_str
会被清除,意味着会释放存储字符串的内存,交还给系统。 -
第二次调用
f
,同样的操作又执行了一边。这就是 C++ 中会触发未定义行为的经典错误:重复释放(double free)。
-
-
而在 Rust 中,编译时检查可以发现上述错误。
-
一个闭包只能被调用一次。
-
闭包必须严格遵守生命期规则,即在调用时,值会被用尽(即转移)。
12.4.2-FnOnce
-
尝试欺骗 Rust,让它两次清除一个
String
。构造如下泛型函数:fn call_twice<F>(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); } };
-
12.4.3-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);
-
12.5 - 回调
-
回调:用户提供的函数,供库在以后调用。
-
以 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) } } }
-
12.6 - 有效使用闭包
-
在 MVC(Model—View—Controller,模型 — 视图 — 控制器)设计模式中,对用户界面上的每个元素,MVC 框架都会创建 3 个对象:模型、视图和控制器。模型表示 UI 元素的状态;视图负责元素的外观;控制器处理用户交互。
- 每个对象都会有另一个或另两个对象的引用。可能是直接引用,也可能是通过回调来引用。
- 在 3 个对象中的一个对象发生了某个事件时,它会通知另外两个对象,因此一切会立即更新。
- 但是,哪个对象拥有其他对象则无法区分。
-
在 Rust 中,必须明确所有权,必须消除循环引用。模型和控制器不能直接相互引用。
-
可以让每个闭包接收它需要的引用作为参数,通过闭包所有权和生命期来解决问题。
-
可以在系统中为每件东西分配一个数值,然后传递数值而不传递引用。
-
可以实现诸多 MVC 变体中的一种,保证对象之间并不是都相互引用。
-
可以仿效某个 非 MVC 系统,比如 Fackbook 的 Flux 架构,实现单向数据流。
from user input -> Action -> Dispatcher -> Store -> View -> to disblay
-
-
迭代器是闭包真正大显身手的主题。
- 可以利用 Rust 闭包的简洁、速度和高效写出不同风格的代码。
详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十四章
原文地址