陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

19|闭包:FnOnce、FnMut和Fn,为什么有这么多类型?

闭包的使用场景
闭包类型
闭包的内存结构
闭包本质
闭包的定义
Rust闭包

该思维导图由 AI 生成,仅供参考

你好,我是陈天。
在现代编程语言中,闭包是一个很重要的工具,可以让我们很方便地以函数式编程的方式来撰写代码。因为闭包可以作为参数传递给函数,可以作为返回值被函数返回,也可以为它实现某个 trait,使其能表现出其他行为,而不仅仅是作为函数被调用。
这些都是怎么做到的?这就和 Rust 里闭包的本质有关了,我们今天就来学习基础篇的最后一个知识点:闭包。

闭包的定义

之前介绍了闭包的基本概念和一个非常简单的例子:
闭包是将函数,或者说代码和其环境一起存储的一种数据结构。闭包引用的上下文中的自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分(第二讲)。
闭包会根据内部的使用情况,捕获环境中的自由变量。在 Rust 里,闭包可以用 |args| {code} 来表述,图中闭包 c 捕获了上下文中的 a 和 b,并通过引用来使用这两个自由变量:
除了用引用来捕获自由变量之外,还有另外一个方法使用 move 关键字 move |args| {code}
之前的课程中,多次见到了创建新线程的 thread::spawn,它的参数就是一个闭包:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Rust中的闭包类型是一种独特的匿名类型,类似于结构体,它包含了捕获的变量。闭包的大小与捕获的变量相关,而不受参数和局部变量的影响。通过分析闭包的内存结构,可以理解为什么thread::spawn对传入的闭包约束是Send + 'static。使用了move且move到闭包内的数据结构满足Send,因为此时,闭包的数据结构拥有所有数据的所有权,它的生命周期是'static。尽管闭包的内存结构看起来并不特别,但与其他语言相比,它的实现方式令人惊讶。Rust的闭包类型设计巧妙,通过所有权和借用的规则解决了闭包在其他语言中常见的生命周期不明确的问题,使得闭包的性能和函数调用相当。闭包在Rust中有三种类型:FnOnce、FnMut和Fn,它们分别代表闭包的不同特性和使用约束。Rust的闭包效率非常高,捕获的变量储存在栈上,没有堆内存分配,而且每个闭包都是一个新的类型,不需要额外的函数指针来运行闭包,因此闭包的调用效率和函数调用几乎一致。文章还通过示例代码展示了不同类型闭包的特点和使用方式,帮助读者更好地理解闭包在Rust中的应用。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(19)

  • 最新
  • 精选
  • D. D
    1. 相当于: struct Closure<'a, 'b: 'a> { data: (i32, i32, i32, i32), v: &'a [&'b str], name: String, } 它的长度等于 4*4(4个i32) + 2*8(ptr, len) + 3*8(ptr, len, cap) = 56字节。 代码的最后不能访问name了,因为已经使用了move关键字将name的所有权移至闭包c中了。 2. 从定义可以看出,调用FnOnce的call_once方法会取得闭包的所有权。因此对于闭包c和c1来说,即使在声明时不使用mut关键字,也可以在其call_once方法中使用所捕获的变量的可变借用。 3. impl<F> Executor for F where F: Fn(&str) -> Result<String, &'static str>, { fn execute(&self, cmd: &str) -> Result<String, &'static str> { self(cmd) } }

    作者回复: 赞!非常好!

    2021-10-06
    2
    29
  • 罗杰
    Rust 闭包,看这一篇真的就够了

    作者回复: :)

    2021-10-06
    5
  • TheLudlows
    思路清晰,深入浅出,佩服陈天老师👍

    作者回复: 谢谢!

    2021-11-11
    2
  • lambda
    关于第三题有个问题,如果我把 impl<F> Executor for F where F: Fn(&str) -> Result<String, &'static str> 写成: impl Executor for fn(&str) -> Result<String, &'static str> 会报错: the trait `Executor` is not implemented for 应该是没对闭包实现Executor这个trait 那我的那个声明是给哪个谁实现了Executor这个trait了呢?

    作者回复: fn 和 Fn / FnMut / FnOnce 不是一回事,fn 是一个 function pointer,不是闭包

    2021-10-23
    3
    1
  • linuxfish
    “然而,一旦它被当做 FnOnce 调用,自己会被转移到 call_once 函数的作用域中,之后就无法再次调用了” 老师,实际调试了一下你的代码,发现只要在`call_once`中传入闭包的引用,后续是可以继续使用闭包的,具体请看: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=27cd35717d166f01a4045846721cf989

    作者回复: 这是因为你传了引用啊,这就不是以 FnOnce 来使用,而是以 Fn 来使用(Fn 实现了 FnOnce 所以 call_once 函数依旧可以工作)。在我的实例代码中 c 本身是一个 Fn。你可以把 name.clone() 那个 clone() 去掉,使其成为 FnOnce,就会发现如果你用 &c 来调用会发生编译错误: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ddaea49ed76de9c9dd856a4c7bcebfe3

    2021-10-07
    4
    1
  • Geek_b52974
    1. 56 2. 傳入 FnOnce 的時候是執行 fn call_once(self, args: Args) -> Self::Output; 是傳入 self, 而非 &mut self 所以不需要 mut 關鍵字 3. impl<F> Executor for F where F: Fn(&str) -> Result<String, &'static str>, { fn execute(&self, cmd: &str) -> Result<String, &'static str> { self(cmd) } }

    作者回复: 👍

    2021-11-04
  • Marvichov
    两点思考, 请老师指正 1. std::function可能类似于dyn Fn()之类的trait object...可能会涉及到额外的vtable (http://www.elbeno.com/blog/?p=1068 提到的optimization也可能优化掉vtable); 不过重点是rust的trait object可以被lifetime 限制. 而cpp不行, 所以std::function需要在heap上得到一个pointer做type erasure 2. 例子中&main的size是0...从Cpp过来的人表示很奇怪...查了一下: main不是function pointer; 而是和closure有点相似的function item的instance (类似于一个zero sized struct, 不过包含了function name, args, lifetimes) ``` // found `fn() {main}` -> closure has unique id, so does main // it also has a struct for it // https://github.com/rust-lang/rust/issues/62440 // size_of_val(main), size_of_val(&main), ``` https://github.com/rust-lang/rust/issues/62440 > This is the compiler's way of representing the unique zero sized type that corresponds to the function. > > This is akin to how closures also create a unique type (but in that case, the size may be >= 0 depending on the captured environment). function item需要被显式coerce到function pointer (https://doc.rust-lang.org/nightly/reference/types/function-item.html)

    作者回复: 👍

    2021-10-12
  • 亚伦碎语
    pub trait Executor { fn execute(&self, cmd: &str) -> Result<String, &'static str>; } struct BashExecutor { env: String, } impl<F> Executor for F where F: Fn(&str) -> Result<String, &'static str>, { fn execute(&self, cmd: &str) -> Result<String, &'static str> { self(cmd) } } impl Executor for BashExecutor { fn execute(&self, cmd: &str) -> Result<String, &'static str> { Ok(format!( "fake bash execute: env: {}, cmd: {}", self.env, cmd )) } } // 看看我给的 tonic 的例子,想想怎么实现让 27 行可以正常执行 fn main() { let env = "PATH=/usr/bin".to_string(); let cmd = "cat /etc/passwd"; let r1 = execute(cmd, BashExecutor { env: env.clone() }); println!("{:?}", r1); let r2 = execute(cmd, |cmd: &str| { Ok(format!("fake fish execute: env: {}, cmd: {}", env, cmd)) }); println!("{:?}", r2); } fn execute(cmd: &str, exec: impl Executor) -> Result<String, &'static str> { exec.execute(cmd) }

    作者回复: 👍 非常好

    2021-10-06
  • 记事本
    1、不能访问,name变量的所有权已经被移动闭包里面去了,move强制导致的 3、pub trait Executor{ fn execute(&self,cmd:&str) ->Result<String,&'static str>; } struct BashExecutor{ env:String } impl Executor for BashExecutor{ fn execute(&self, cmd:&str) ->Result<String,&'static str> { Ok(format!( "fake bash execute:env:{},cmd :{}",self.env,cmd )) } } impl <F> Executor for F where F:Fn(&str) ->Result<String,&'static str> { fn execute(&self, cmd:&str) ->Result<String,&'static str> { self(cmd) } } fn execute(cmd:&str,exec:impl Executor) -> Result<String,&'static str>{ exec.execute(cmd) } pub fn test(){ let env = "PATH=/usr/bin".to_string(); let cmd = "cat /etc/passwd"; let r1 = execute(cmd, BashExecutor{env:env.clone()}); println!("{:?}",r1); let r2 = execute(cmd, |cmd :&str|{ Ok(format!("fake fish execute: env: {}, cmd: {}", env, cmd)) }); println!("{:?}",r2); }

    作者回复: 👍非常好!

    2021-10-06
    2
  • f
    发现了老师文中的一个错误结论。当闭包不使用move时,是推断着判断如何去捕获变量的,先尝试不可变引用,然后尝试可变引用,最后尝试Move/Copy,一旦尝试成功,将不再尝试。当使用move时,是强制Move/Copy,而不是一步一步地去推断尝试。 在the rust reference: https://doc.rust-lang.org/reference/expressions/closure-expr.html里有说明: ``` Without the move keyword, the closure expression infers how it captures each variable from its environment, preferring to capture by shared reference, effectively borrowing all outer variables mentioned inside the closure's body. If needed the compiler will infer that instead mutable references should be taken, or that the values should be moved or copied (depending on their type) from the environment. A closure can be forced to capture its environment by copying or moving values by prefixing it with the move keyword. This is often used to ensure that the closure's lifetime is 'static. ``` 代码验证: ```rust fn main() { let mut name = String::from("hello"); // 1.不可变引用,&name被存储在闭包c1里 let c1 = || &name; // 可使用所有者变量name,且可多次调用闭包 println!("{}, {:?}, {:?}", name, c1(), c1()); // 2.可变引用,&mut name被存储在闭包c2里,调用c2的时候要修改这个字段, // 因此c2也要设置为mut c2 let mut c2 = || { name.push_str(" world "); }; // 可多次调用c2闭包 // 但不能调用c2之前再使用name或引用name,因为&mut name已经存入c2里了 // println!("{}", name); // 取消注释将报错 // println!("{}", &name); // 取消注释将报错 c2(); c2(); // 3.Move/Copy,将name移入到闭包c3中 let c3 = || { let x = name; // let y = name; // 取消注释见报错,use of moved value }; // println!("{}", name); //取消注释将报错 } ```
    2021-10-06
    2
    21
收起评论
显示
设置
留言
19
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部