Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >rust闭包(Closure)

rust闭包(Closure)

作者头像
zy010101
发布于 2023-04-27 07:06:49
发布于 2023-04-27 07:06:49
69100
代码可运行
举报
文章被收录于专栏:程序员程序员
运行总次数:0
代码可运行

闭包(Closure)

闭包在现代化的编程语言中普遍存在。闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值。Rust 闭包在形式上借鉴了 Smalltalk 和 Ruby 语言,与函数最大的不同就是它的参数是通过 |parm1| 的形式进行声明,如果是多个参数就 |param1, param2,…|, 下面给出闭包的形式定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
|param1, param2,...| {
    语句1;
    语句2;
    返回表达式
}

如果只有一个返回表达式的话,定义可以简化为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
|param1| 返回表达式
  1. 闭包可以接受0个或者多个参数,写在||里面;
  2. 闭包体中是实际执行的代码;
  3. 闭包可以捕获任何它周围作用域中的变量,并在闭包体中使用。

闭包的类型推导

Rust 是静态语言,因此所有的变量都具有类型,但是得益于编译器的强大类型推导能力,在很多时候我们并不需要显式地去声明类型,但是显然函数并不在此列,必须手动为函数的所有参数和返回值指定类型,原因在于函数往往会作为 API 提供给你的用户,因此你的用户必须在使用时知道传入参数的类型和返回值类型。

与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。

为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let sum = |x: i32, y: i32| -> i32 {
    x + y
}

下面展示了同一个功能的函数和闭包实现形式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn  add_one_v1   (x: u32) -> u32 { x + 1 }      // 函数
let add_one_v2 = |x: u32| -> u32 { x + 1 };     // 闭包形式1
let add_one_v3 = |x|             { x + 1 };     // 闭包形式2
let add_one_v4 = |x|               x + 1  ;     // 闭包形式3

三种不同的闭包也展示了三种不同的使用方式:省略参数、返回值类型和花括号对。

结构体中的闭包

假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:

一个闭包用于获取值 一个变量,用于存储该值 可以使用结构体来代表缓存对象,最终设计如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    query: T,
    value: Option<u32>,
}

这里的T的特征约束Fn(u32) -> u32有点像C++中声明函数指针类型,标准库提供的 Fn 系列特征,再结合特征约束,就能很好的解决了这个问题. T: Fn(u32) -> u32 意味着 query 的类型是 T,该类型必须实现了相应的闭包特征 Fn(u32) -> u32。约束表明该闭包拥有一个u32类型的参数,同时返回一个u32类型的值。Fn 特征不仅仅适用于闭包,还适用于函数,因此上面的 query 字段除了使用闭包作为值外,还能使用一个具名的函数来作为它的值。下面的例子将上面的u32类型换成了泛型,可以用来缓存各种数据类型。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
use std::fmt::Debug;

#[allow(unused)]
fn main() {
    let x = "Hello World!".to_string();
    let mut cache = Cacher::new(|x| -> String {x});
    cache.value(x);
    println!("{:?}", cache.value);

    let x = 123;
    let mut cache = Cacher::new(|x| -> i32 {x});
    cache.value(x);
    println!("{:?}", cache.value);
}

struct Cacher<T, E>
where
    T: Fn(E) -> E,
    E: Clone + Debug
{
    query: T,
    value: Option<E>,
}

impl<T, E> Cacher<T, E>
where
    T: Fn(E) -> E,
    E: Clone + Debug
{
    fn new(query: T) -> Self{
        Cacher { query, value: None}
    }

    fn value(&mut self, arg: E) -> E {
        match self.value.clone() {
            Some(v) => v,
            None => {
                let v = (self.query)(arg);
                self.value = Some(v.clone());
                v
            }
        }
    }
}

上面这段代码有以下几点需要注意。

  1. Cacher结构体有一个value字段,类型是Option<T>,同时Cacher还拥有一个value方法。(因此在rust里,set和get操作,就是给字段和方法起同一个名字,我的评价是不如C#)
  2. 给E的特征约束是Clone+Debug,这样无论对于什么类型,都可以进行复制和打印输出。
  3. 需要注意let v = (self.query)(arg)这行,前面的self.query必须用括号包括起来,否则会编译器会报错。
  4. 闭包捕获了String类型的x的时候,并没有导致所有权转移,(因为之后我们正常调用了cache.value(x))需要知道这里发生了什么。

捕获引用或者移动所有权

闭包可以通过三种方式捕获作用域中的值,它们直接对应到函数获取参数的三种方式:不可变借用,可变借用和获取所有权。闭包会根据函数体中如何使用被捕获的值决定用哪种方式捕获。这点非常抽象,不如C++的lambda表达式简单易懂。

以不可变引用方式捕获

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn main() {
    let mut x = "Hello".to_string();
    let lambda = || println!("{}", x);      // 在闭包对变量x的操作是只读,因此rust会使用不可变引用方式来捕获
    lambda();  // 使用闭包
    x.push_str(", World!");
    println!("{}", x);
}

在来看一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn main() {
    let mut x = "Hello".to_string();
    let lambda = || println!("{}", x);      // 在闭包对变量x的操作是只读,因此rust会使用不可变引用方式来捕获
    lambda();   // 使用闭包,(其中存在x的不可变引用)
    x.push_str(", World!");
    println!("{}", x);      
    lambda();   // 再次调用lambda
}

这个例子无法通过编译,这是因为编译器检查到在同一作用域内,既有可变引用,又有不可变引用。最后一次调用lambda的时候,其中存在x的不可变引用,而之前的x.push_str又是一个可变引用。具体的报错如下所示:

报错中很直接的指出既有mutable又有immutable。 2. 以可变引用方式捕获

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn main() {
    let mut x = "Hello".to_string();
    let mut lambda = || x.push_str(", World");
    lambda();
    x.push_str("-- from zhangsan");
    println!("{}", x);
}

这个例子中,我们在闭包中对捕获的x做了修改,因此rust会以可变引用的方式捕获,需要注意的是lambda必须是可变的才行。另外我们在调用了lambda之后,又使用了push_str来修改x,编译成功通过。这是因为rust的编译器检测到lambda不再使用,直接被drop掉了。因此当前作用域内只有一个可变引用,而不是两个可变引用。我们可以通过下面的例子来证实这一点。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn main() {
let mut x = "Hello".to_string();
let mut lambda = || x.push_str(", World!");
lambda();
x.push_str("-- from zhangsan");
lambda();   // 再次调用lambda
}

这段代码无法通过编译,原因是同一作用域内存在两个可变引用,这是rust不允许的。具体错误如下图所示。

以转移所有权方式捕获

下面这个例子用来展示转移所有权方式捕获。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn func(s: String) {
    println!("{s}");
}

fn main() {
    let x = "Hello".to_string();
    let lambda = || func(x);
    lambda();
}

我们在闭包中调用了func函数,将x的所有权转移到了func函数中。x随着func函数调用结束而释放。我们来使用一个例子来证实。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn func(s: String) {
    println!("{s}");
}

fn main() {
    let x = "Hello".to_string();
    let lambda = || func(x);
    lambda();
    println!("{}", x);
}

编译报错信息如下所示:

错误显示我们借用了一个moved之后的值。因此会失败。同时上面这种方式也会导致只能调用一次lambda闭包。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn func(s: String) {
    println!("{s}");
}

fn main() {
    let x = "Hello".to_string();
    let lambda = || func(x);
    lambda();
    lambda();	// ERROR
}

同时注意到编译器提示我们this value implements FnOnce, which causes it to be moved when called。说我们的lambda实现了FnOnce trait,在调用时会发生所有权移动。因为随着x的所有权被转移到func函数中,它已经随着第一次func函数调用而被释放。如果我们想要既能捕获环境中变量的所有权,又能多次调用,需要使用关键字move,它将环境中的变量所有权转移到闭包中。在将闭包传递到一个新的线程时这个技巧很有用,它可以移动数据所有权给新线程。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn func(s: &String) {	// 传递String类型的引用
    println!("{s}");
}

fn main() {
    let x = "Hello".to_string();
    let lambda = move || func(&x);		// move
    lambda();
    lambda();	// 第二次调用lambda
}

这样,我们的第二次调用lambda就是成功的。为了验证x确实被移动走了,我们在调用一次lambda之后增加一行打印。来看看程序执行是否出错,如果出错那就证明x被移走了,否则x没有被移走。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn func(s: &String) {
    println!("{s}");
}

fn main() {
    let x = "Hello".to_string();
    let lambda = move || {func(&x)};
    lambda();
    println!("{}", x);		// ERROR		value borrowed here after move
    lambda();
}

程序执行报错,表明x确实被移走了。因此我们无法在println!中打印这个x。不过此时还有一个疑问,那就是x可以被转移到闭包内,它的生命周期和闭包本身是一样的,而闭包的生命周期就是它最后一次被调用的时候

Fn trait

闭包捕获和处理环境中的值的方式影响闭包实现的 trait。Trait 是函数和结构体指定它们能用的闭包的类型的方式。取决于闭包体如何处理值,闭包自动、渐进地实现一个、两个或三个 Fn trait。

  1. FnOnce 适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。它只能被调用一次,在第一次调用后被释放。
  2. FnMut 适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次。
  3. Fn 适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要。

函数也可以实现所有的三种 Fn traits。如果我们要做的事情不需要从环境中捕获值,则可以在需要某种实现了 Fn trait 的东西时使用函数而不是闭包。下面的例子展示了Fn trait的用法,并且这个例子中充满了陷阱。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Human<T>
    where T: FnOnce() -> String
{
    name: T
}

impl<T> Human<T>
    where T: FnOnce() -> String
{
    fn new(name: T) -> Self {
        Human { name }
    }
    fn get_name(self) -> String{
        (self.name)()
    }
}


fn main() {
    let name = "zhangsan".to_string();
    let lambda = || name;
    let h = Human::new(lambda);

    println!("{}", h.get_name());
}

首先,由于FnOnce的trait约束,lambda闭包将以转移所有权的方式捕获环境中的name,并且闭包第一次调用后,被释放; 其次,get_name中的参数是self,意味着将h的所有权转移到get_name中,随着get_name调用结束,h被释放。我们再来看一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn main() {
    let s = "Hello".to_string();

    let update_string = || println!("{}",s);

    exec(update_string);
}

fn exec<F: FnOnce()>(f: F)  {
    f();
}

这个例子中的,闭包以不可变引用的方式捕获了s,exec的参数f要求的约束是FnOnce。因此f只能被调用一次,如果在exec中多次调用f,那么编译器会提示你加上Copy trait。

实际上,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

  1. 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  2. 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  3. 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征

下面这个例子很好的说明这一点。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn main() {
    let s = "Hello".to_string();

    let update_string =  || println!("{}",s);

    exec(update_string);
    exec1(update_string);
    exec2(update_string);
    
}

fn exec<F: FnOnce()>(f: F)  {
    f()
}

fn exec1<F: FnMut()>(mut f: F)  {
    f()
}

fn exec2<F: Fn()>(f: F)  {
    f()
}

虽然,闭包只是对 s 进行了不可变借用,实际上,它可以适用于任何一种 Fn 特征:三个 exec 函数说明了一切。一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量。下面是三个Fn trait的简化版源码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

从特征约束能看出来 Fn 的前提是实现 FnMut,FnMut 的前提是实现 FnOnce,因此要实现 Fn 就要同时实现 FnMut 和 FnOnce。从源码中还能看出一点:Fn 获取 &self,FnMut 获取 &mut self,而 FnOnce 获取 self。 在实际项目中,建议先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择

参考资料

  1. Rust语言圣经
  2. Rust 程序设计语言
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Rust闭包的虫洞穿梭
闭包(Closure)的概念由来已久。无论哪种语言,闭包的概念都被以下几个特征共同约束:
袁承兴
2020/09/19
1.4K0
Rust学习笔记Day23 闭包的使用场景,3种常用闭包类型有哪些
Output: 是FnOnce的关联类型,是闭包的返回值类型。call_once: 第一个参数是self,它会转移self的所有权到call_once函数里。Args: 是泛型参数。
用户1072003
2023/02/23
6650
Rust学习笔记Day23 闭包的使用场景,3种常用闭包类型有哪些
go 开发者的 rust 入门
即:在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。引用必须总是有效的。
王磊-字节跳动
2021/11/27
1.9K0
Rust中move、copy、clone、drop和闭包捕获
rust中的类型,如果没有实现Copy trait,那么在此类型的变量赋值、函数入参、函数返回值都是move语义。这是与c++的最大区别,从c++11开始,右值引用的出现,才有了move语义。但rust天生就是move语义。
MikeLoveRust
2020/05/14
1.5K0
深入浅出理解Rust闭包
Rust中闭包的设计目标是要快:比函数指针还要快,快到甚至可以在对性能敏感的热点代码中使用它们
草帽lufei
2024/07/20
1290
深入浅出理解Rust闭包
一名Java开发的Rust学习笔记
笔者的主力语言是Java,近三年Kotlin、Groovy、Go、TypeScript写得比较多。早年间还写过一些Python和JavaScript。总得来说落地在生产中的语言都是应用级语言,对于系统编程级语言接触不多。但这不妨碍我写下这么一篇笔记,说不定也有一些常年在应用层的同学想领略一下Rust的风采呢。
泊浮目
2024/03/19
2610
一名Java开发的Rust学习笔记
【Rust 基础篇】Rust 闭包
在 Rust 中,闭包(closures)是一种函数对象,它可以捕获其环境中的变量,并在需要时调用。闭包提供了一种方便的方式来封装行为,并在需要时进行调用。本篇博客将详细介绍 Rust 中的闭包,包括闭包的定义、语法、捕获变量的方式以及一些常见的使用场景。
繁依Fanyi
2023/10/12
4290
Rust学习笔记Day22 何为闭包?闭包的本质是什么?
我以前以为闭包就是 当前作用域的一个临时函数。作者说闭包可以方便的函数式编程。闭包
用户1072003
2023/02/23
6450
Rust学习笔记Day22 何为闭包?闭包的本质是什么?
Rust学习:如何解读函数签名?
在Rust中,函数签名类似“讲故事”。经验丰富的Rust程序员,只需浏览一个函数的签名,就可以知道该函数大部分的行为。
MikeLoveRust
2019/09/03
2.2K0
【Rust每周一知】如何理解Rust的默认线程安全?
本文以Rc和RefCell为例,讨论Rust中的Send和Sync是如何保证线程安全的。
MikeLoveRust
2020/02/12
1.5K0
2023学习日志
可以通过std::env::var函数获取环境变量,该函数的返回结果为Result类型,可以通过is_ok方法来判断环境变量是否被设置。当环境变量被设置时,is_ok方法返回true,否则返回false。
TomoriNao
2023/07/09
1330
【投稿】原创:以新视角,解读【闭包】
概括地讲,我这篇文章就是总结了上述(3)与(4)项中提到的“条件”关系于一张表格,并基于该表格展开论述。
MikeLoveRust
2022/03/28
4300
【投稿】原创:以新视角,解读【闭包】
【译】为 嵌入式 C 程序员编写的 Rust 指南
这是来自 Google OpenTitan 团队,给嵌入式 C 程序员专门打造的一份 Rust 指南。
张汉东
2021/10/13
5.3K0
rust智能指针
智能指针虽然也号称指针,但是它是一个复杂的家伙:通过比引用更复杂的数据结构,包含比引用更多的信息,例如元数据,当前长度,最大可用长度等。引用和智能指针的另一个不同在于前者仅仅是借用了数据,而后者往往可以拥有它们指向的数据,然后再为其它人提供服务。智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:
zy010101
2023/05/09
1.1K0
【Rust 基础篇】Rust 线程与 Move 闭包
Rust 是一门以安全性著称的系统编程语言,它允许程序员高效地进行并发编程。在 Rust 中,线程是一种重要的并发原语,通过标准库提供的 std::thread 模块,我们可以轻松地创建和管理线程。而 Move 闭包是一种特殊的闭包,它可以在创建时携带外部变量的所有权,使得在多线程环境中传递数据更加灵活和高效。本篇博客将详细介绍 Rust 中线程和 Move 闭包的使用方法,包含代码示例和对定义的详细解释。
繁依Fanyi
2023/10/12
4790
《Rust避坑式入门》第1章:挖数据竞争大坑的滥用可变性
赵可菲是一名Java程序员,一直在维护一个有十多年历史的老旧系统。这个系统即将被淘汰,代码质量也很差,每次上线都会出现很多bug,她不得不加班修复。公司给了她3个月的内部转岗期,如果转不出去就会被裁员。她得知公司可能会用Rust重写很多系统,于是就报名参加了公司的Rust培训,希望能够转型。
程序员吾真本
2024/08/29
5810
《Rust避坑式入门》第1章:挖数据竞争大坑的滥用可变性
「转自 InfoQ」Rust:一个不再有 C/C++ 的,实现安全实时软件的未来
Rust 作为新兴编程语言深受 Haskell 和 OCaml 等函数式编程语言的影响,使得它在语法上与 C++ 类似,但在语义上则完全不同。Rust 是静态类型语言,同时具有完整类型推断,而不是 C++ 的部分类型推断,它在速度上可与 C++ 媲美的同时,也保证了内存安全。
MikeLoveRust
2019/10/15
1.2K0
「转自 InfoQ」Rust:一个不再有 C/C++ 的,实现安全实时软件的未来
Rust语法入门
Rust 是一种系统级编程语言,它的设计目标是提供高性能、安全性和并发性。Rust 的主要优势包括:
码客说
2023/04/17
1.3K0
【翻译】Rust生命周期常见误区
我曾经有过的所有这些对生命周期的误解,现在有很多初学者也深陷于此。我用到的术语可能不是标准的,所以下面列了一个表格来解释它们的用意。
MikeLoveRust
2020/07/28
1.7K0
Rust - 安装环境、基本类型、流程控制、函数、模块、泛型、所有权
学习Rust语言是公司同事最先开始提议的,准备用接下来的项目试试水,Rust是一个强类型编译型语言,比较偏向底层,所以开启了Rust的探索之旅。
stark张宇
2023/02/24
1.2K0
相关推荐
Rust闭包的虫洞穿梭
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验