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

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

|参数| {函数体}

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

引言

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

rust 复制代码
let a = 0;

// 一般函数
// fn f1 () -> i32 {a} // 报错:fn中无法捕获动态环境变量

// 闭包
let f2 = || println("{}", a); // 闭包捕获&a
let f3 = |a: i32|{}; // 闭包啥都没捕获,a只是个普通的形参

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

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

print_me();

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

rust 复制代码
#[derive(Clone, Copy)]
struct __closure_1__<'a> { // note: lifetime parameter
    message: &'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: 只能调用一次,存在捕获的变量所有权转移被消耗
rust 复制代码
// 闭包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时发生值拷贝

rust 复制代码
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时发生所有权转移

rust 复制代码
let a = "".to_string();
// impl Fn()
let f1 = move || println!("{}", a); // 环境中变量a对应值的所有权转移给了闭包a
// 因为并未产生消耗,所以类型推导仍然是Fn,f1可以反复调用
f1();
f1();
// println("{}", a); // 报错,使用了值已发生move的a

let mut b = "".to_string();
// impl FnMut()
let mut f2 = move || {
	b += "x";
	println("{}", b);
};
f2(); // x
f2(); // xx
// println("{}", b); // 报错,使用了值已发生move的b

let c = "".to_string();
// impl FnOnce()
let f3 = move || {
	println("{}", c);
	std::mem::drop(c); // 这边有没有move其实都一样,闭包drop未实现Copy的值,默认捕获的就是转移了所有权的环境变量
};
f3(); 

(3)一些需要注意的点

  • 闭包中,若环境变量直接作为返回值,会以值的形式返回 [1]
rust 复制代码
// 实现了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";  // 捕获所有权转移的b
	b // 没有";" 返回所有权转移的b; 因为所有权发生转移,并作为返回值传递(消费),所以无法反复调用,故类型推导是FnOnce
}
f2();
  • 有些场景会对未实现Copy的变量触发隐式的move (没有找到相关的资料,暂且只能靠记忆)
rust 复制代码
// 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,三种皆可

定义:

rust 复制代码
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() -> () {}

调用:

rust 复制代码
// 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;
rust 复制代码
// 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,需要注意所有权转移的可能
rust 复制代码
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]

rust 复制代码
is_fn_mut(&mut f2);
is_fn_mut(&mut f2);

相关资料:

1\] [users.rust-lang.org/t/closure-c...](https://link.juejin.cn?target=https%3A%2F%2Fusers.rust-lang.org%2Ft%2Fclosure-capture-by-borrowing-is-not-a-regular-reference%2F55945%2F8 "https://users.rust-lang.org/t/closure-capture-by-borrowing-is-not-a-regular-reference/55945/8") \[2\] [rustwiki.org/zh-CN/std/k...](https://link.juejin.cn?target=https%3A%2F%2Frustwiki.org%2Fzh-CN%2Fstd%2Fkeyword.move.html "https://rustwiki.org/zh-CN/std/keyword.move.html") \[3\] Additional implementors 其他实现者 英 [doc.rust-lang.org/core/marker...](https://link.juejin.cn?target=https%3A%2F%2Fdoc.rust-lang.org%2Fcore%2Fmarker%2Ftrait.Copy.html "https://doc.rust-lang.org/core/marker/trait.Copy.html") 中 [rustwiki.org/zh-CN/std/m...](https://link.juejin.cn?target=https%3A%2F%2Frustwiki.org%2Fzh-CN%2Fstd%2Fmarker%2Ftrait.Copy.html "https://rustwiki.org/zh-CN/std/marker/trait.Copy.html") \[4\] [rustwiki.org/zh-CN/std/o...](https://link.juejin.cn?target=https%3A%2F%2Frustwiki.org%2Fzh-CN%2Fstd%2Fops%2Ftrait.FnMut.html "https://rustwiki.org/zh-CN/std/ops/trait.FnMut.html")

相关推荐
YiSLWLL2 小时前
使用Tauri 2.3.1+Leptos 0.7.8开发桌面小程序汇总
python·rust·sqlite·matplotlib·visual studio code
yu4106212 小时前
Rust 语言使用场景分析
开发语言·后端·rust
朝阳5816 小时前
Rust项目GPG签名配置指南
开发语言·后端·rust
朝阳5816 小时前
Rust实现高性能目录扫描工具ll的技术解析
开发语言·后端·rust
红尘散仙10 小时前
六、WebGPU 基础入门——Vertex 缓冲区和 Index 缓冲区
前端·rust·gpu
苏近之10 小时前
深入浅出 Rust 异步运行时原理
rust·源码
红尘散仙11 小时前
四、WebGPU 基础入门——Uniform 缓冲区与内存对齐
前端·rust·gpu
大卫小东(Sheldon)14 小时前
魔方求解器桌面版(层先法,基于Tauri实现)
rust
景天科技苑16 小时前
【Rust结构体】Rust结构体详解:从基础到高级应用
开发语言·后端·rust·结构体·关联函数·rust结构体·结构体方法
苏近之16 小时前
说明白 Rust 中的泛型: 泛型是一种多态
设计模式·rust