【译】理解Rust中的闭包
原文标题:Understanding Closures in Rust
原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f286ed1759
公众号: Rust 碎碎念
翻译 by: Praying
概要
闭包(closure)是函数指针(function pointer)和上下文(context)的组合。 没有上下文的闭包就是一个函数指针。 带有不可变上下文(immutable context)的闭包属于 Fn
带有可变上下文(mutable context)的闭包属于 FnMut
拥有其上下文的闭包属于 FnOnce
理解 Rust 中不同类型的闭包

不同于其他语言,Rust 对self
参数的使用是显式的。当我们实现结构体时,必须把self
作为函数签名的第一个参数:
struct MyStruct {
text: &'static str,
number: u32,
}
impl MyStruct {
fn new (text: &'static str, number: u32) -> MyStruct {
MyStruct {
text: text,
number: number,
}
}
// We have to specify that 'self' is an argument.
fn get_number (&self) -> u32 {
self.number
}
// We can specify different kinds of ownership and mutability of self.
fn inc_number (&mut self) {
self.number += 1;
}
// There are three different types of 'self'
fn destructor (self) {
println!("Destructing {}", self.text);
}
}
因此,下面这两种风格是一样的:
obj.get_number();
MyStruct::get_number(&obj);
这和那些把self
(或this
)隐藏起来的语言不同。在那些语言中,只要将一个函数和一个对象或结构关联起来,就隐含着第一个参数是self
。在上面的代码中,我们有四个self
选项:一个不可变引用,一个可变引用,一个被拥有的值,或者压根就没有使用self
作为参数。
因此,self
表示函数执行的某一类上下文。它在 Rust 中是显式的,但是在其他语言中经常是隐含的。
此外,在本文中,我们将会使用下面的函数:
fn is_fn <A, R>(_x: fn(A) -> R) {}
fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}
这些函数的唯一作用是类型检查。例如,如果is_FnMut(&func)
能够编译,那么我们就可以知道那个func
属于FnMut
trait。
无上下文和fn
(小写的 f)类型
鉴于上述内容,考虑几个使用(上面定义的)MyStruct
的闭包的例子:
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let closure1 = |x: &MyStruct| x.get_number() + 3;
assert_eq!(closure1(&obj1), 18);
assert_eq!(closure1(&obj2), 13);
这是我们可以得到的最简单的(代码示例)。这个闭包为类型MyStruct
的任意对象中的已有数字加上三。它可以在任意位置被执行,不会有任何问题,并且编译器不会给你带来任何麻烦。我们可以很简单地写出下面这样的代码替代closure1
:
// It doesn't matter what code appears here, the function will behave
// exactly the same.
fn func1 (x: &MyStruct) -> u32 {
x.get_number() + 3
}
assert_eq!(func1(&obj1), 18);
assert_eq!(func1(&obj2), 13);
这个函数不依赖于它的上下文。无论它之前和之后发生了什么,它的行为都是一致的。我们(几乎)可以互换着使用func1
和closure1
。
当一个闭包完全不依赖上下文时,我们的闭包的类型就是fn
:
// compiles successfully.
is_fn(closure1);
is_Fn(&closure1);
is_FnMut(&closure1);
is_FnOnce(&closure1);
可变上下文和Fn
(大写的 F)trait
相较于上面,我们可以为闭包添加一个上下文。
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure immutably.
let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
assert_eq!(closure2(&obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR
closure2
依赖于obj1
的值,并且包含周围作用域的信息。在这段代码中,closure2
将会借用obj1
从而使得它可以在函数体中使用obj1
。我们还可以对obj1
进行不可变借用,但是如果我们试图在后面修改obj1
,我们将会得到一个借用错误。
如果我们尝试使用fn
语法来重写我们的闭包,我们在函数内部需要知道的一切都必须作为参数传递给函数,所以我们添加了一个额外的参数来表示函数的上下文:
struct Context<'a>(&'a MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(&obj1);
fn func2 (context: &Context, x: &MyStruct) -> u32 {
x.get_number() + context.0.get_number()
}
其行为和我们的闭包基本一致:
assert_eq!(func2(&ctx, &obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR
注意,Context
结构体包含一个对MyStruct
结构体的不可变引用,这表明我们将无法在函数内部修改它。
当我们调用closure1
时,也意味着我们我们把周围的上下文作为参数传递给了closure1
,正如我们在使用fn
时必须要做的那样。在一些其他的语言中,我们不必指定将self
作为参数传递,与之类似,Rust 也不需要我们显式地指定将上下文作为参数传递。
当一个闭包以不可变引用的方式捕获上下文,我们称它实现了Fn
trait。这表明我们可以不必修改上下文而多次调用我们的函数。
// Does not compile:
// is_fn(closure2);
// Compiles successfully:
is_Fn(&closure2);
is_FnMut(&closure2);
is_FnOnce(&closure2);
可变上下文和FnMut
trait
如果我们在闭包内部修改obj1
,我们会得到不同的结果:
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure mutably.
let mut closure3 = |x: &MyStruct| {
obj1.inc_number();
x.get_number() + obj1.get_number()
};
assert_eq!(closure3(&obj2), 26);
assert_eq!(closure3(&obj2), 27);
assert_eq!(closure3(&obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18); // ERROR
// obj1.inc_number(); // ERROR
这一次我们不能对obj1
进行可变借用和不可变借用了。我们还必须得把闭包标注为mut
。如果我们希望使用fn
语法重写这个函数,将会得到下面的代码:
struct Context<'a>(&'a mut MyStruct);
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let mut ctx = Context(&mut obj1);
// obj1 is borrowed by the closure mutably.
fn func3 (context: &mut Context, x: &MyStruct) -> u32 {
context.0.inc_number();
x.get_number() + context.0.get_number()
};
其行为与closure3
相同:
assert_eq!(func3(&mut ctx, &obj2), 26);
assert_eq!(func3(&mut ctx, &obj2), 27);
assert_eq!(func3(&mut ctx, &obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18); // ERROR
// obj1.inc_number(); // ERROR
注意,我们必须把我们的上下文以可变引用的方式传递。这表明每次调用我们的函数后,可能会得到不同的结果。
当闭包以可变引用捕获它的上下文时,我们称它属于FnMut
trait。
// Does not compile:
// is_fn(closure3);
// is_Fn(&closure3);
// Compiles successfully:
is_FnMut(&closure3);
is_FnOnce(&closure3);
拥有的上下文
在我们的最后一个例子中,我们将会获取obj1
的所有权:
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is owned by the closure
let closure4 = |x: &MyStruct| {
obj1.destructor();
x.get_number()
};
我们必须在使用closure4
之前检查它的类型:
// Does not compile:
// is_fn(closure4);
// is_Fn(&closure4);
// is_FnMut(&closure4);
// Compiles successfully:
is_FnOnce(&closure4);
现在我们可以检查它的行为:
assert_eq!(closure4(&obj2), 10);
// We can't call closure4 twice...
// assert_eq!(closure4(&obj2), 10); //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15); // ERROR
// obj1.inc_number(); // ERROR
在这个例子中,我们只能调用这个函数一次。一旦我们对它进行了第一次调用,obj1
就被我们销毁了, 所以它在第二次调用的时候也就不复存在了。Rust 会给我们一个关于使用一个已经被移动的值的错误。这就是为什么我们要事先检查其类型。
使用fn
语法来实现,我们可以得到下面的代码:
struct Context(MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(obj1);
// obj1 is owned by the closure
fn func4 (context: Context, x: &MyStruct) -> u32 {
context.0.destructor();
x.get_number()
};
这段代码,正如我们所预期的,和我们的闭包行为一致:
assert_eq!(func4(ctx, &obj2), 10);
// We can't call func4 twice...
// assert_eq!(func4(ctx, &obj2), 10); //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15); // ERROR
// obj1.inc_number(); // ERROR
当我们使用fn
来写我们的闭包时,我们必须使用一个Context
结构体并拥有它的值。当一个闭包拥有其上下文的所有权时,我们称它实现了FnOnce
。我们只能调用这个函数一次,因为在调用之后,上下文就被销毁了。
总结
不需要上下文的函数拥有
fn
类型,并且可以在任意位置调用。仅需要不可变地访问其上下文的函数属于
Fn
trait,并且只要上下文在作用域中存在,就可以在任意位置调用。需要可变地访问其上下文的函数实现了
FnMut
trait,可以在上下文有效的任意位置调用,但是每次调用可能会做不同的事情。获取上下文所有权的函数只能被调用一次。

【译】理解Rust中的闭包的更多相关文章
- 浅谈 .NET 中的对象引用、非托管指针和托管指针 理解C#中的闭包
浅谈 .NET 中的对象引用.非托管指针和托管指针 目录 前言 一.对象引用 二.值传递和引用传递 三.初识托管指针和非托管指针 四.非托管指针 1.非托管指针不能指向对象引用 2.类成员指针 五 ...
- 【译】理解Rust中的Futures(二)
原文标题:Understanding Futures in Rust -- Part 2 原文链接:https://www.viget.com/articles/understanding-futur ...
- 【译】深入理解Rust中的生命周期
原文标题:Understanding Rust Lifetimes 原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes- ...
- 【译】理解Rust中的局部移动
原文标题:Understanding Partial Moves in Rust 原文链接:https://whileydave.com/2020/11/30/understanding-partia ...
- 【译】理解Rust中的Futures (一)
原文标题:Understanding Futures In Rust -- Part 1 原文链接:https://www.viget.com/articles/understanding-futur ...
- 两个函数彻底理解Lua中的闭包
本文通过两个函数彻底搞懂Lua中的闭包,相信看完这两个函数,应该能理解什么是Lua闭包.废话不多说,上 code: --[[************************************** ...
- 【译】Rust中的array、vector和slice
原文链接:https://hashrust.com/blog/arrays-vectors-and-slices-in-rust/ 原文标题:Arrays, vectors and slices in ...
- 【原】理解javascript中的闭包
闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 什么是闭包? 官方说法: 闭包是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见 ...
- [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()
译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...
随机推荐
- shell-的变量-局部变量
1. 定义本地变量 本地变量在用户当前的shell生产期的脚本中使用.例如,本地变量OLDBOY取值为ett098,这个值只在用户当前shell生存期中有意义.如果在shell中启动另一个进程或退出, ...
- 《Android逆向反编译代码注入》 - 逆向安全入门必看视频教程
适合人群: Android开发人员.逆向反编译开发人员.以及对Android逆向安全感兴趣的朋友. 视频地址: 51CTO学院:https://edu.51cto.com/course/24485 ...
- kafka配置文件详解
kafka的配置分为 broker.producter.consumer三个不同的配置 一 .BROKER 的全局配置最为核心的三个配置 broker.id.log.dir.zookeeper.con ...
- Apache Jmeter 性能测试
今天在写性能测试报告的时候需要使用到数据,打算用做一下性能测试,然后在百度后发现了一款Apache开源的Jmeter压测工具 Jmeter概述: Apache JMeter是一款纯java编写负载功能 ...
- 【手摸手,带你搭建前后端分离商城系统】02 VUE-CLI 脚手架生成基本项目,axios配置请求、解决跨域问题
[手摸手,带你搭建前后端分离商城系统]02 VUE-CLI 脚手架生成基本项目,axios配置请求.解决跨域问题. 回顾一下上一节我们学习到的内容.已经将一个 usm_admin 后台用户 表的基本增 ...
- IP协议那些事
IP协议作为通信子网的最高层.提供无连接的数据报传输机制. IP协议的作用 寻址和路由 传递服务:提供不可靠,无连接的服务. 为什么说IP协议不可靠.无连接 不可靠:是指不能保证IP数据包能成成功到达 ...
- 【C语言编程入门笔记】排序算法之快速排序,一文轻松掌握快排!
排序算法一直是c语言重点,各个算法适应不用的环境,同时,在面试时,排序算法也是经常被问到的.今天我们介绍下快速排序,简称就是快排. 1.快速排序思想: 快排使用 分治法 (Divide and con ...
- 【图论】USACO11JAN Roads and Planes G
题目内容 洛谷链接 Farmer John正在一个新的销售区域对他的牛奶销售方案进行调查.他想把牛奶送到\(T\)个城镇 (\(1 <= T <= 25,000\)),编号为\(1\)到\ ...
- 用cgroup限制内存以防止Linux因内存用尽卡死
Linux在内存用尽的情况下,整个界面,包括tty和ctrl-alt-F1都会卡住难以响应.虽然Linux内核有OOM Killer机制杀掉吃内存的进程,但经常内存用尽时连OOM Killer都无法动 ...
- 解决/lib/ld-linux.so.2: bad ELF interpreter: No such file or directory报错 (转)
解决/lib/ld-linux.so.2: bad ELF interpreter: No such file or directory报错 念淅 2020-01-03 15:02:25 3793 收 ...