Rust闭包 - Fn/FnMut/FnOnce traits,捕获和传参

2023-10-20 02:30

本文主要是介绍Rust闭包 - Fn/FnMut/FnOnce traits,捕获和传参,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Rust闭包: 是一类能够 捕获周围作用域中变量 的 函数


|参数| {函数体}

  • 参数及返回值类型可推导,无需显示标注
  • 类型唯一性,确定后不可更改
  • 函数体为单个表达式时,{}可省略

文章目录

    • 引言
    • 1 分类 Fn / FnMut / FnOnce
    • 2 关键词 move
    • 3 闭包作为参数传递

引言

闭包区别于一般函数最大的特点就是,可以捕获周围作用域(不一定是当前同作用域,上级也可以)中的变量;当然,也可以选择啥都不捕获。

let a = 0;// 一般函数
// fn f1 () -> i32 {a} // 报错:fn中无法捕获动态环境变量// 闭包
let f2 = || println("{}", a); // 闭包捕获&a
let f3 = |a: i32|{}; // 闭包啥都没捕获,a只是个普通的形参

这里说的捕获不应该认为是像函数一样简单地传参,可以理解成闭包也是一种语法糖,它背后进行的操作要复杂的多,详细可参考文末相关资料[1]

// 举个栗子,定义了以下闭包并调用
let message = "Hello World!".to_string();
let print_me = || println!("{}", message);print_me();

其实际进行的操作是这样:

#[derive(Clone, Copy)]
struct __closure_1__<'a> { // note: lifetime parametermessage: &'a String, // note: &String, 下文会提到所谓的——捕获引用
}impl<'a> Fn<()> for __closure_1__<'a> {// type Output = ();fn call(&self, (): ()) -> () {println!("{}", *self.message)}
}let message = "Hello World!".to_string();
let print_me = __closure_1__ { message: &message };Fn::call(&print_me, ());

1 分类 Fn / FnMut / FnOnce

根据捕获变量进行的操作,Rust里的闭包实现的traits共三种
注意!这里的因果关系,是捕获变量的操作 决定 闭包实现的形式

  • Fn : 可在不改变状态的情况下重复调用; 捕获变量的不可变引用(shared reference)或啥都不捕获
  • FnMut: 可改变状态,可重复调用; 捕获变量的可变引用(mutable reference
  • FnOnce: 只能调用一次,存在捕获的变量所有权转移被消耗
// 闭包impl trait编译器会自动根据捕获操作推导,注释方便阅读
let a = 0;
// impl Fn()
let f1 = || println("{}", a); // 捕获&a
f1();
f1();let mut b = 0;
// impl FnMut()
let mut f2 = || b+=1; // 捕获&mut b; 可能会有疑问为什么不需要解引用*b+=1, 参考相关资料[1]
f2();
f2();let c = "".to_string();
// impl FnOnce()
let f3 = || std::mem::drop(c);
f3();
//f3(); // 报错,f3只能调用一次,c所有权已经发生了转移并且消费了它

2 关键词 move

move将引用或可变引用捕获的任何变量转换为按值捕获的变量
注意!闭包实现的traits是由对值进行的操作确定,而不是捕获值的方式;这意味即使闭包中捕获的是值,发生了所有权转移,它也可能是FnFnMut [2]

(1) 实现Copy trait的对象,move时发生值拷贝

let a = 0;
// impl Fn()
let f1 = move || println("{}", a); // 将捕获的不可变引用转换为值拷贝传递给闭包let mut b = 0;
// impl FnMut()
let mut f2 = move || b += 1;
f2();
f2();
println("{}", b); // 因为闭包里是值拷贝,所以还是0

(2)未实现Copy trait的对象,move时发生所有权转移

let a = "".to_string();
// impl Fn()
let f1 = move || println!("{}", a); // 环境中变量a对应值的所有权转移给了闭包a
// 因为并未产生消耗,所以类型推导仍然是Fn,f1可以反复调用
f1();
f1();
// println("{}", a); // 报错,使用了值已发生move的alet mut b = "".to_string();
// impl FnMut()
let mut f2 = move || {b += "x";println("{}", b);
};
f2(); // x
f2(); // xx
// println("{}", b); // 报错,使用了值已发生move的blet c = "".to_string();
// impl FnOnce()
let f3 = move || {println("{}", c);std::mem::drop(c); // 这边有没有move其实都一样,闭包drop未实现Copy的值,默认捕获的就是转移了所有权的环境变量
};
f3(); 

(3)一些需要注意的点

  • 闭包中,若环境变量直接作为返回值,会以值的形式返回 [1]
// 实现了Copy类型的数据
let mut a = 0;
// impl FnMut() -> i32
let mut f1  = || {a += 1// 捕获a引用a // 没有";" 闭包类型推导的返回值是i32
}; 
f1();
f1();
println!("{}", a); // 2// 未实现Copy类型的数据
let mut b = "".to_string();
// impl FnOnce() -> String
let mut f2 = || {b += "x";  // 捕获所有权转移的bb // 没有";" 返回所有权转移的b; 因为所有权发生转移,并作为返回值传递(消费),所以无法反复调用,故类型推导是FnOnce
}
f2();
  • 有些场景会对未实现Copy的变量触发隐式的move
    (没有找到相关的资料,暂且只能靠记忆)
// std::mem::drop 参考之前的例子// path statement
let a = "".to_string();
// impl FnOnce() 
let f1 = || {a;}; // operation statement
let b = "".to_string();
// impl FnOnce()
let f2 = || {b+"x";};

3 闭包作为参数传递

Fn 继承自 FnMut 继承自 FnOnce
在这里插入图片描述
根据继承关系可以得到结论:

  • 当形参类型为Fn时,只能传递Fn
  • 当形参类型为FnMut时,可以传递 Fn, FnMut
  • 当形参类型为FnOnce,三种皆可

定义:

fn is_fn<F>(_: F) where F: Fn() -> () {}fn is_fn_mut<F>(_: F) where F: FnMut() -> () {}fn is_fn_once<F>(_: F) where F: FnOnce() -> () {}

调用:

// impl Fn()
let f1 = || {};let mut count = 0;
// impl FnMut()
let mut f2 = || count += 1;let s = "".to_string();
// impl FnOnce()
let f3 = || std::mem::drop(s);is_fn(f1);is_fn_mut(f1);
is_fn_mut(&mut f2);is_fn_once(f1);
is_fn_once(&mut f2);
is_fn_once(f3);

注意!!!这里不能调用 is_fn_mut(f2)
原因是闭包本身作为Fn*类型的数据,也是要考虑其本身Copy trait的实现:参考[3]

  • 若未发生捕获,或捕获的是值拷贝,或只进行了不可变的引用(shared reference),那么闭包本身也实现了Copy trait;
// impl Fn(), 未捕获
let fn_f1 = || {}; 
is_fn(fn_f1);
is_fn(fn_f1);// impl FnMut(), 捕获值拷贝
let mut a = 0;
let mut fnmut_f2 = move || count1 += 1; 
is_fn_mut(fnmut_f2);
is_fn_mut(fnmut_f2);// impl Fn(), 捕获不可变引用
let b = 0;
let fn_f3 = || println("", b);
is_fn(fn_f3);
is_fn(fn_f3);
  • 若捕获的是可变引用(mutable reference),那么闭包本身则未实现Copy trait,需要注意所有权转移的可能
fn is_fn_mut<F>(_: F) where F: FnMut() -> () {}let mut count = 0;
// impl FnMut()
let mut f2 = || count += 1;
is_fn_mut(f2); // 仅调用一次没问题,但是此时f2所有权已经发生了move
//is_fn_mut(f2); // 报错,使用了发生move的f2

想要多次调用的话,需传递&mut f2&mut F也是实现了FnMut的,所以这里传递引用没有问题,参考[4]

is_fn_mut(&mut f2);
is_fn_mut(&mut f2);


相关资料:
[1] https://users.rust-lang.org/t/closure-capture-by-borrowing-is-not-a-regular-reference/55945/8
[2] https://rustwiki.org/zh-CN/std/keyword.move.html
[3] Additional implementors 其他实现者
英 https://doc.rust-lang.org/core/marker/trait.Copy.html
中 https://rustwiki.org/zh-CN/std/marker/trait.Copy.html
[4] https://rustwiki.org/zh-CN/std/ops/trait.FnMut.html

这篇关于Rust闭包 - Fn/FnMut/FnOnce traits,捕获和传参的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/244026

相关文章

Rust中的Option枚举快速入门教程

《Rust中的Option枚举快速入门教程》Rust中的Option枚举用于表示可能不存在的值,提供了多种方法来处理这些值,避免了空指针异常,文章介绍了Option的定义、常见方法、使用场景以及注意事... 目录引言Option介绍Option的常见方法Option使用场景场景一:函数返回可能不存在的值场景

【Rust练习】12.枚举

练习题来自:https://practice-zh.course.rs/compound-types/enum.html 1 // 修复错误enum Number {Zero,One,Two,}enum Number1 {Zero = 0,One,Two,}// C语言风格的枚举定义enum Number2 {Zero = 0.0,One = 1.0,Two = 2.0,}fn m

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(

第二十四章 rust中的运算符重载

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust中的运算符重载 目录 注意一、前言二、基本使用三、常用运算符四、通用约束 一、前言 C/C++中有运算符重载这一概念,它的目的是让即使含不相干的内容也能通过我们自定义的方法进行运算符操作运算。 比如字符串本身是不能相加的,但由于C++中的String重载了运算符+,所以我们就可以将两个字符串进行相加、但实际

JavaScript深入理解闭包

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域。 变量的作用域无非就是两种:全局变量和局部变量。 Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。 Js代码   var n=999;   function f1

如何使用Selenium捕获控制台日志

Selenium是一个流行的开源工具,用于自动化Web浏览器。其中一个关键功能是能够与浏览器的开发者控制台交互。本文将向您展示如何在Selenium中使用Java获取控制台日志。这些日志对于调试和解决Selenium脚本的问题非常有用。 如何查看任何网页的控制台日志 首先,打开浏览器的开发者控制台。在大多数浏览器中,您可以通过右键点击页面并选择“检查”来做到这一点。我们将在我们的测试网站——h

Vue3 父子传参 简单易懂

在Vue 3中,父组件向子组件传递数据(也称为“props”)是一个非常常见的模式。这是通过props选项在子组件中定义的,然后在父组件的模板中使用该子组件时通过属性(attributes)传递数据。 步骤 1: 定义子组件的props 首先,在子组件中定义你希望从父组件接收的props。 vue复制代码 <!-- ChildComponent.vue --> <template> <

八、我们应当怎样做需求调研:需求捕获(下)

前面我们讨论了,需求分析工作是一个迭代的过程:需求捕获->需求整理->需求验证->再需求捕获······需求捕获是这个迭代过程的开始,也是整个需求分析工作中最重要的部分。没有捕获哪来后面的整理与验证工作?但是,非常遗憾,按照我以往的经验,需求捕获是我们最薄弱的环节。前面我提到的许许多多项目开发的问题都可以归结为需求分析的问题,而许许多多需求分析的问题又都可以归结为需求捕获不完整的问题。需求捕获是整

七、我们应当怎样做需求调研:需求捕获(上)

前面我们讨论了,需求分析工作是一个迭代的过程:需求捕获->需求整理->需求验证->再需求捕获······需求捕获是这个迭代过程的开始,也是整个需求分析工作中最重要的部分。没有捕获哪来后面的整理与验证工作?但是,非常遗憾,按照我以往的经验,需求捕获是我们最薄弱的环节。前面我提到的许许多多项目开发的问题都可以归结为需求分析的问题,而许许多多需求分析的问题又都可以归结为需求捕获不完整的问题。需求捕获是整

【Rust光年纪】Rust 机器人学库全景:功能、安装与API概览

机器人学+Rust语言=无限可能:六款库带你开启创新之旅! 前言 随着机器人技术的快速发展,对于机器人学领域的高效、可靠的编程语言和库的需求也日益增加。本文将探讨一些用于 Rust 语言的机器人学库,以及它们的核心功能、使用场景、安装配置和 API 概览,旨在为机器人学爱好者和开发人员提供参考和指导。 欢迎订阅专栏:Rust光年纪 文章目录 机器人学+Rust语言=无限可能: