ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rust 闭包实战:以函数作为输入参数——深入理解 `Fn`/`FnMut`/`FnOnce` 与泛型约束

Rust 闭包实战:以函数作为输入参数——深入理解 `Fn`/`FnMut`/`FnOnce` 与泛型约束 文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载闭包closure可以捕获外层环境的变量也可以像普通函数一样被调用而本篇文章要解决的核心问题是闭包能作为函数参数普通函数是否也能答案是肯定的——只要满足闭包 trait 约束Fn、FnMut、FnOnce的函数都可以与闭包一样作为参数传入。本文将基于 rust-by-example 仓库中 输入函数章节 的完整示例结合同目录下的 输入参数、捕获规则、类型匿名 与 输出参数 等章节为你讲透三大闭包 trait 的语义差异、编译器如何选择捕获方式以及编写接受可调用对象的泛型函数时的完整实战方案。1. 核心问题函数可以作为闭包参数传入吗闭包可以作为参数使用那么函数呢确实可以在 input_functions.md 中给出了直接答案如果你声明了一个以闭包为参数的函数那么任何满足该闭包 trait 约束trait bound的函数都可以作为参数传入。这一点非常实用闭包与普通函数在可作为参数传递这一点上是统一的只要它实现了对应的闭包 trait。来看文档中的完整示例// 定义一个接受泛型 F 参数的函数约束为 Fn并调用它 fn call_meF: Fn()(f: F) { f(); } // 定义一个满足 Fn 约束的包装函数 fn function() { println!(Im a function!); } fn main() { // 定义一个满足 Fn 约束的闭包 let closure || println!(Im a closure!); call_me(closure); call_me(function); }这段代码的运行结果是依次打印Im a closure!和Im a function!。注意几个关键点call_meF: Fn()中的F: Fn()表示F是一个泛型类型参数且必须实现Fntrait且签名是无参数、无返回值closure无捕获的闭包与function普通函数都满足Fn()约束因此都能传入call_me这说明Fn系列 trait 是闭包与函数之间的统一接口——二者在作为参数时地位相同。2. 三大闭包 traitFn、FnMut、FnOnce的语义文档特别补充了一条附加说明Fn、FnMut、FnOnce这三个 trait 决定了闭包如何从外层作用域捕获变量。在 input_parameters.md 中给出了按约束严格程度递减的完整列表Trait捕获方式说明Fn按不可变引用T约束最严格只能读取捕获值FnMut按可变引用mut T可以修改捕获值FnOnce按值T约束最宽松可消费consume捕获值且只能调用一次文档强调了两条核心规律逐变量选择最宽松方式编译器会在变量级variable-by-variable为每个被捕获的变量选择尽可能宽松即限制最小的捕获方式。FnOnce的特殊性如果一个参数被标注为FnOnce意味着闭包可能按T、mut T或T捕获最终由编译器根据闭包体内对捕获变量的实际使用方式决定。方向不可逆如果按值移动是可能的那么任何形式的借用也一定可能move 蕴含 borrow 的能力但反过来不成立——如果参数被标注为Fn那么按mut T或T捕获都是不允许的只有T被允许。2.1 实战演练切换三种约束观察编译器行为input_parameters.md 提供了一个互动示例你可以尝试将F: FnOnce()换成Fn或FnMut观察编译结果// 一个以闭包为参数并调用它的函数。 // F 表示 F 是一个 泛型类型参数 fn applyF(f: F) where // 闭包不接受输入且不返回任何内容 F: FnOnce() { // ^ TODO: 尝试将这里改为 Fn 或 FnMut。 f(); } // 一个接受闭包并返回 i32 的函数。 fn apply_to_3F(f: F) - i32 where // 闭包接受一个 i32 并返回一个 i32 F: Fn(i32) - i32 { f(3) } fn main() { use std::mem; let greeting hello; // 一个非 Copy 类型 // to_owned 从借用的数据创建自有数据owned data let mut farewell goodbye.to_owned(); // 捕获 2 个变量greeting 按引用farewell 按值 let diary || { // greeting 按引用需要 Fn println!(I said {}., greeting); // 修改操作强制 farewell 按可变引用捕获。现在需要 FnMut farewell.push_str(!!!); println!(Then I screamed {}., farewell); println!(Now I can sleep. zzzzz); // 手动调用 drop 强制 farewell 按值捕获。现在需要 FnOnce mem::drop(farewell); }; // 调用应用该闭包的函数 apply(diary); // double 满足 apply_to_3 的 trait 约束 let double |x| 2 * x; println!(3 doubled: {}, apply_to_3(double)); }这个例子完美展示了约束与使用方式之间的张力greeting只被println!读取因此编译器按T捕获最低要求是Fnfarewell被push_str修改捕获升级为mut T最低要求是FnMutfarewell最后被mem::drop消费drop 要求按值拥有T捕获升级为按值移动T此时最低要求是FnOnce因此整个diary闭包只能用FnOnce约束传入apply。如果你把它改成Fn或FnMut约束编译就会失败因为闭包体内存在需要FnOnce的用法。3. 闭包类型为什么是匿名的理解参数为何必须用泛型要真正理解函数作为输入参数还需要弄明白闭包类型为什么不能用普通函数签名来表达。在 anonymity.md 中给出了解释当定义一个闭包时编译器会隐式地创建一个新的匿名结构体来存储被捕获的变量并通过Fn、FnMut或FnOnce这三个 trait 之一为新类型实现调用功能。由于这个新类型对我们是未知匿名的任何将闭包作为参数的函数都必须使用泛型但无约束的类型参数T依然有歧义、不被允许因此用Fn/FnMut/FnOnce之一闭包实际实现的 trait作为约束就足以指定它的类型。// F 必须实现 Fn对应一个无输入、无返回值的闭包—— // 这正是 print 所需要的 fn applyF(f: F) where F: Fn() { f(); } fn main() { let x 7; // 将 x 捕获进匿名类型并为它实现 Fn存入 print let print || println!({}, x); apply(print); }3.1 三种约束写法等价吗在 input_parameters.md 中使用了where子句写法在 input_functions.md 中使用了内联写法// 内联写法 fn call_meF: Fn()(f: F) { f(); } // where 子句写法 fn applyF(f: F) where F: FnOnce() { f(); }两种写法语义等价where写法在约束较多时更易读。仓库中泛型相关的完整讲解见 泛型章节 与 where 子句章节。4. 从捕获规则到约束选择编译器如何决定捕获方式捕获方式与 trait 约束是一体两面。在 capture.md 中文档详细演示了编译器优先按引用捕获只有在必要时才降低到更严格的捕获方式只读使用 → 不可变借用Fn级别color仅被println!使用闭包print只持有不可变引用因此闭包存在期间color仍可再次被不可变借用甚至在print最后一次使用之后还可以整体移动color修改使用 → 可变借用FnMut级别inc闭包对count做 1编译器选择mut count比按值移动限制更小因此inc必须声明为mut且在闭包仍会被调用期间无法对count进行重新借用消费使用 → 按值移动FnOnce级别mem::drop需要按值拥有T因此movable立即移入闭包consume该闭包只能被调用一次。fn main() { use std::mem; let color String::from(green); // 只读借用Fn 级别 let print || println!(color: {}, color); print(); // 闭包只持不可变引用color 仍可被不可变借用 let _reborrow color; print(); // print 最后一次使用之后color 可以整体移动 let _color_moved color; let mut count 0; // 可变借用FnMut 级别 let mut inc || { count 1; println!(count: {}, count); }; inc(); // inc 仍持有 mut count此时重新借用会报错 // let _reborrow count; inc(); // 闭包不再需要 mut count 后可以重新借用 let _count_reborrowed mut count; // 按值移动FnOnce 级别 let movable Box::new(3); let consume || { println!(movable: {:?}, movable); mem::drop(movable); }; consume(); // consume(); // 已被移动再次调用会编译错误 }理解这条规律后写以闭包为参数的函数时就能准确判断约束写得越宽如FnOnce能传入的可调用对象越多写得越窄如Fn调用方自由度越高但限制也越多。通常的工程实践是只消费调用能力用FnOnce可能修改状态用FnMut纯只读回调用Fn。4.1move关键字强制按值捕获在 capture.md 中还提到在竖线前使用move会强制闭包以拥有ownership方式捕获所有变量fn main() { // Vec 具有非 Copy 语义 let haystack vec![1, 2, 3]; let contains move |needle| haystack.contains(needle); println!({}, contains(1)); println!({}, contains(4)); // println!(Therere {} elements in vec, haystack.len()); // ^ 取消注释会编译报错变量已移入闭包借用检查器不允许再使用 }如果不加move闭包会对haystack做不可变借用原变量仍可用加上move后haystack所有权转移给闭包原作用域内不能再使用。这在多线程std::thread::spawn要求捕获值必须static等场景中是必需手段。5. 类型层级速查什么时候该用哪个 trait综合 input_parameters.md 与 capture.md 的内容可以用一句话概括三者的包含关系Fn的类型 ⊂FnMut的类型 ⊂FnOnce的类型从能做的最少的事角度看约束越宽泛能传入的对象越多。标注FnOnce的参数可以接受按T、mut T、T捕获的闭包以及普通函数指针标注FnMut的参数可以接受按T、mut T捕获的闭包和函数但不能接受需要按值移动的闭包标注Fn的参数只接受按T捕获的闭包纯只读和普通函数。6. 延伸闭包作为返回值时的impl Trait与move理解了输入方向的约束再看输出方向会豁然开朗。在 output_parameters.md 中说明闭包类型匿名返回闭包必须使用impl Trait合法的返回 trait 同样只有Fn、FnMut、FnOnce并且必须加move——否则按引用捕获的变量会在函数退出时被 drop留下悬垂引用fn create_fn() - impl Fn() { let text Fn.to_owned(); move || println!(This is a: {}, text) } fn create_fnmut() - impl FnMut() { let text FnMut.to_owned(); move || println!(This is a: {}, text) } fn create_fnonce() - impl FnOnce() { let text FnOnce.to_owned(); move || println!(This is a: {}, text) } fn main() { let fn_plain create_fn(); let mut fn_mut create_fnmut(); let fn_once create_fnonce(); fn_plain(); fn_mut(); fn_once(); }关于impl Trait的完整讲解见仓库中的 impl Trait 章节。再加上泛型 generics 与 高阶函数HOF即接收一个或多个函数、或产出一个更有用函数的函数如Iterator::take_while、filter等Rust 的函数式风格就完整地串起来了闭包捕获 → trait 约束选择 → 泛型参数传入 →impl Trait返回这一整套链路都建立在Fn/FnMut/FnOnce三兄弟之上。7. 总结与实践建议回到开头的问题——函数能否作为闭包参数传入可以只要它满足对应的闭包 trait 约束。记住以下要点统一抽象闭包和普通函数在Fn系列 trait 下是同一抽象fn call_meF: Fn()(f: F)既能接闭包也能接函数约束决定捕获Fn/FnMut/FnOnce分别对应T、mut T、T三种捕获方式编译器会按需选择最宽松的方式并据此推断闭包实现的最低 trait泛型是必然选择闭包类型匿名参数只能用泛型 闭包 trait 约束来表达返回闭包用impl Trait move避免悬垂引用动手验证把 input_parameters.md 示例中的FnOnce改成Fn/FnMut再编译观察错误信息是理解这套语义最直观的方式。所有示例都标注了rust,editable可以在 rust-by-example 的在线代码编辑器Live code editor中直接运行修改。相关文档索引主章节 Closures、捕获规则、输入参数、类型匿名、输出参数、高阶函数。赞分享文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载相关推荐Rust 闭包作为输入参数Fn / FnMut / FnOnce 三大约束的完整实践指南Rust 闭包作为输入参数Fn / FnMut / FnOnce 三大约束的完整实践指南 闭包Closure在 Rust 中可以在创建时自由选择以 T文档教程Embla Carousel 实战零依赖轻量轮播方案Embla Carousel 实战零依赖轻量轮播方案 写商品轮播时你往往有三个诉求拖拽要准、过渡要顺、别引入一个动辄几百 KB 的依赖。Embla Car密码学彻底搞懂Rust闭包Fn/FnMut/FnOnce实战指南彻底搞懂Rust闭包Fn/FnMut/FnOnce实战指南 你还在为Rust闭包的三种类型感到困惑吗为什么有时闭包能多次调用有时却会报错use of m编程语言编译器语言运行时标准库上一篇StreamCap覆盖40平台的直播自动录制工具下一篇如何把碧蓝航线的 Live2D 模型保存到本地AzurLaneLive2DExtract 使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表