19|闭包:FnOnce、FnMut和Fn,为什么有这么多类型?
该思维导图由 AI 生成,仅供参考
闭包的定义
- 深入了解
- 翻译
- 解释
- 总结
Rust中的闭包类型是一种独特的匿名类型,类似于结构体,它包含了捕获的变量。闭包的大小与捕获的变量相关,而不受参数和局部变量的影响。通过分析闭包的内存结构,可以理解为什么thread::spawn对传入的闭包约束是Send + 'static。使用了move且move到闭包内的数据结构满足Send,因为此时,闭包的数据结构拥有所有数据的所有权,它的生命周期是'static。尽管闭包的内存结构看起来并不特别,但与其他语言相比,它的实现方式令人惊讶。Rust的闭包类型设计巧妙,通过所有权和借用的规则解决了闭包在其他语言中常见的生命周期不明确的问题,使得闭包的性能和函数调用相当。闭包在Rust中有三种类型:FnOnce、FnMut和Fn,它们分别代表闭包的不同特性和使用约束。Rust的闭包效率非常高,捕获的变量储存在栈上,没有堆内存分配,而且每个闭包都是一个新的类型,不需要额外的函数指针来运行闭包,因此闭包的调用效率和函数调用几乎一致。文章还通过示例代码展示了不同类型闭包的特点和使用方式,帮助读者更好地理解闭包在Rust中的应用。
《陈天 · Rust 编程第一课》,新⼈⾸单¥68
全部留言(19)
- 最新
- 精选
- D. D1. 相当于: 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-06229 - 罗杰Rust 闭包,看这一篇真的就够了
作者回复: :)
2021-10-065 - TheLudlows思路清晰,深入浅出,佩服陈天老师👍
作者回复: 谢谢!
2021-11-112 - 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-2331 - 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-0741 - Geek_b529741. 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-062 - 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-06221