尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Comprehensive Rust 教程:深入理解 Rust 闭包三大特征(Fn / FnMut / FnOnce)与函数指针

Comprehensive Rust 教程:深入理解 Rust 闭包三大特征(Fn / FnMut / FnOnce)与函数指针 Comprehensive Rust 教程深入理解 Rust 闭包三大特征Fn / FnMut / FnOnce与函数指针【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust闭包是 Rust 中最灵活也最常被误解的语言特性之一闭包拥有编译器生成的匿名类型却能通过Fn、FnMut、FnOnce三个标准库特征被泛型代码统一调用。本篇以 Google Android 团队开源的 Rust 课程Comprehensive Rust中 src/closures/traits.md 为骨架结合课程配套的 Log Filter 练习 与参考答案系统讲解三大闭包特征的区别、子类型关系、捕获方式共享引用 / 独占引用 / move以及函数指针的转换规则帮助你掌握「闭包参数到底该声明成哪个特征」这一 API 设计核心问题。为什么闭包类型无法被命名在 Rust 中闭包closure又称 lambda 表达式的类型是匿名的、无法被写出来的。每次写出|x| x 1编译器都会生成一个独一无二的内部类型它甚至带有自动推断的捕获字段。这就是为什么我们无法像声明fn foo(...)那样去声明一个闭包变量——你只能写let double_it |n| n * 2; // 类型由编译器推断无法手写为了让泛型代码能够接收并调用这些匿名类型标准库提供了三个可调用特征callable traitsFn、FnMut和FnOnce。任何闭包都实现了这三个特征中的至少一个具体的实现取决于它捕获了什么、以及如何捕获。与之相对fn(..) - T注意小写fn表示函数指针function pointer它可以指向一个普通函数的地址也可以指向一个什么都不捕获的闭包。函数指针类型是具体、可命名的但约束也更强——只有无捕获的闭包才能转换为函数指针。三大特征逐个拆解课程原文 src/closures/traits.md 给出了一个能一次性演示三种捕获形态的完整示例我们逐段分析。FnOnce可以调用一次的特征FnOnce是三者中最宽松的基准特征。实现它的闭包只能被调用一次——调用本身会消费consume这个闭包以及所有被按值捕获move 捕获的变量。fn apply_and_log( func: impl FnOnce(static str) - String, func_name: static str, input: static str, ) { println!(Calling {func_name}({input}): {}, func(input)) } fn main() { let suffix -itis; let add_suffix |x| format!({x}{suffix}); apply_and_log(add_suffix, add_suffix, senior); apply_and_log(add_suffix, add_suffix, appendix); }这里apply_and_log的参数被声明为impl FnOnce(static str) - String意味着调用方可以传入任意可调用一次的函数对象——这是对调用方最宽松的接口设计。函数体内的func(input)即触发一次调用由于FnOnce只保证能调用一次编译器允许它消费捕获值。FnMut可以重复调用但会修改捕获状态当一个闭包修改了它捕获的变量它就至少是FnMut。FnMut通过独占引用exclusive reference访问闭包对象因此可以被反复调用但不能并发调用。fn main() { let mut v Vec::new(); let mut accumulate |x| { v.push(x); v.join(/) }; apply_and_log(mut accumulate, accumulate, red); apply_and_log(mut accumulate, accumulate, green); apply_and_log(mut accumulate, accumulate, blue); }注意这里两件事accumulate修改了捕获的v因此它实现的是FnMut调用方必须使用mut accumulate独占引用才能调用它而apply_and_log之所以能接受mut accumulate是因为**FnMut是FnOnce的子类型**——它满足FnOnce的能调用一次的要求。当三次调用结束后v中的内容依次为[red]、[red, green]、[red, green, blue]返回值分别是red、red/green、red/green/blue闭包在多次调用之间保持了内部状态。Fn可以共享引用调用甚至并发调用Fn是最强的特征实现它的闭包既不消费也不修改捕获值只需一个共享引用shared reference即可调用。因此它可以被无限次调用甚至安全地并发调用例如多线程同时调用。fn main() { let suffix -itis; let add_suffix |x| format!({x}{suffix}); apply_and_log(add_suffix, add_suffix, senior); apply_and_log(add_suffix, add_suffix, appendix); }上面的add_suffix只以共享引用的方式捕获了suffix所以它实现Fn。注意调用时传入的是add_suffix共享引用这就是Fn与FnMut在调用方式上的直观差别。演示 FnOnce 的一次性消费fn main() { let mut v Vec::new(); let mut accumulate |x| { v.push(x); v.join(/) }; apply_and_log(mut accumulate, accumulate, red); apply_and_log(mut accumulate, accumulate, green); apply_and_log(mut accumulate, accumulate, blue); let take_and_reverse |prefix| { let mut acc String::from(prefix); acc.push_str(v.into_iter().rev().collect::Vec_().join(/)); acc }; apply_and_log(take_and_reverse, take_and_reverse, reversed: ); }take_and_reverse的闭包体内执行了v.into_iter()——这把v按值移动进了闭包因此它只实现FnOnce。它被直接按值传给apply_and_log没有加调用一次后闭包连同它捕获的v一起被消费。输出为reversed: blue/green/red从后往前拼接正是rev()的结果。子类型关系与特征自动推导课程原文用一个简洁的结论概括了三者的关系FnMut是FnOnce的子类型Fn是FnMut与FnOnce的子类型。也就是说凡是需要FnOnce的地方都可以传入FnMut凡是需要FnMut或FnOnce的地方都可以传入Fn。这是一条单向的能力降级链Fn能力最强调用方式限制最小⊂FnMut⊂FnOnce能力最弱但限制最多。编译器根据闭包实际捕获的内容自动推导它实现哪个些特征只读捕获共享引用→ 实现Fn同时自然实现FnMut、FnOnce修改捕获独占引用→ 实现FnMut同时实现FnOnce按值消费捕获move→ 只能实现FnOnce。此外编译器还会自动推断Copy与Clone不捕获任何可变状态的闭包如add_suffix可以Copy捕获了String等类型的闭包如take_and_reverse则是Clone。而函数指针对fn项的引用始终实现Copy和Fn。设计建议该声明哪个特征这是本节最实战的部分。课程给出了两条方向相反的规则写函数接收闭包的一方时能小则小。当定义一个接收闭包的函数时如果只调用一次就用FnOnce否则用FnMut实在不行才用Fn。理由声明越宽松的特征调用方越灵活——调用方可以用Fn、FnMut或FnOnce任意形态的闭包满足FnOnce参数但只有Fn形态的闭包能满足Fn参数。impl FnOnce(...)是最开放的接口签名。持有闭包作为调用方时能大则大。当你手里有一个闭包时最灵活的形式是Fn可以传给任意一个接受三种特征之一的消费者其次是FnMut最不灵活的是FnOnce。原因正好反过来一个Fn闭包可以交给任何消费者而一个FnOnce闭包只能交给接受FnOnce的消费者。实战验证Log Filter 练习中的 Fn 边界为了让这些原则落地课程安排了「Log Filter」练习见 src/closures/exercise.md 与参考答案 src/closures/solution.md。练习要求基于早上的泛型Logger实现一个用闭包过滤日志消息的Filterpub trait Logger { fn log(self, verbosity: u8, message: str); } struct FilterL, P { inner: L, predicate: P, } implL, P FilterL, P where L: Logger, P: Fn(u8, str) - bool, { fn new(inner: L, predicate: P) - Self { Self { inner, predicate } } } implL, P Logger for FilterL, P where L: Logger, P: Fn(u8, str) - bool, { fn log(self, verbosity: u8, message: str) { if (self.predicate)(verbosity, message) { self.inner.log(verbosity, message); } } } fn main() { let logger Filter::new(StderrLogger, |_verbosity, msg| msg.contains(yikes)); logger.log(5, FYI); logger.log(1, yikes, something went wrong); logger.log(2, uhoh); }这个练习从三个层面印证了本节的知识点闭包必须用泛型参数存储FilterL, P中的P是闭包的匿名类型占位。因为每个闭包类型都独一无二且无法命名所以只能通过泛型或Boxdyn Fn..特征对象来持有。为什么边界必须是FnLogger::log接收的是self在log内部我们只能不可变地访问self.predicate。若把边界改成FnMut就必须把log改成mut self这与Logger特征签名冲突若改成FnOnce则每次log调用都会消费掉 predicate整条 Logger 只能记录一条消息。只有Fn能同时满足重复调用与不可变访问两个约束。这也是上一节能小则小原则的反向印证这里的log(self)签名强迫我们选择最严格但最安全可行的Fn。调用字段闭包的括号细节(self.predicate)(verbosity, message)中包围self.predicate的括号是必需的否则编译器会把它解析成调用一个名为predicate的方法而不是调用这个字段本身。注意new的impl块也带上边界有助于类型推断。运行时输出只有一条消息verbosity1: yikes, something went wrong因为 predicate|_verbosity, msg| msg.contains(yikes)过滤掉了不含 yikes 的日志。闭包捕获机制速览要完整理解三大特征还需明白捕获的默认规则详见课程的 src/closures/capturing.md闭包默认按引用捕获环境变量且会为每个变量选择最低要求的访问形式优先共享引用其次独占引用最后才按值移动move。如果闭包体修改了捕获变量如max_value 1它就会被捕获为独占引用特征随之升级为FnMut。使用move关键字写作move |v| ..可以强制按值捕获这在闭包需要活得比捕获值更久涉及生命周期时非常有用——例如在std::thread::spawn(move || ...)中把变量移入新线程这在课程的并发章节如 src/concurrency/channels/bounded.md中被大量使用。不捕获任何变量的闭包可以零成本转换为函数指针fn(..) - T参考 src/closures/syntax.md此时它就实现了Fn与Copy。小结特征捕获形态调用方式可调用次数并发安全FnOnce按值move消费捕获按值传入仅一次——FnMut独占引用修改捕获mut多次不可并发Fn共享引用只读捕获无限次可并发记忆要点写函数收闭包选FnOnce→FnMut→Fn的顺序从宽到严持有闭包交出去选Fn最灵活。这一章是后续学习Iterator、thread::spawn、异步任务tokiospawn 要求Send static等高级特性的基石——理解了捕获与特征推导你就理解了为什么某些闭包能跨线程、某些不能、某些只能调用一次。课程中这一节属于 Day 2 的闭包模块完整目录见 src/SUMMARY.md与syntax.md、capturing.md、exercise.md共同构成一套完整的闭包教学单元。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表