ARTICLE DETAIL

资讯详情

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

comprehensive-rust 闭包实战:用 `Fn` trait bound 在结构体中存储与调用闭包(Log Filter 练习全解析)

comprehensive-rust 闭包实战:用 `Fn` trait bound 在结构体中存储与调用闭包(Log Filter 练习全解析) comprehensive-rust 闭包实战用Fntrait bound 在结构体中存储与调用闭包Log Filter 练习全解析【免费下载链接】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本篇技术指南以 Google Android 团队的开源 Rust 课程 comprehensive-rust 中「Closures」章节的课后练习Log Filter及其官方解答 solution.md 为核心完整讲解如何把一个闭包作为谓词存储进结构体字段、如何通过泛型与Fntrait bound 约束它、以及如何在self方法中安全地调用它。读完本文你将掌握在 Rust 中存储闭包的标准模式、Fn/FnMut/FnOnce三兄弟的取舍逻辑以及(self.predicate)(...)这种字段调用的写法与原因。练习背景为通用 Logger 增加过滤能力本练习建立在课程当天上午的通用日志器基础上。练习原文见 exercise.md核心目标是实现一个Filter它使用一个闭包作为过滤谓词只有通过该谓词筛选的日志消息才会被转发给内部的日志器。练习给出的骨架代码如下pub trait Logger { /// Log a message at the given verbosity level. fn log(self, verbosity: u8, message: str); } struct StderrLogger; impl Logger for StderrLogger { fn log(self, verbosity: u8, message: str) { eprintln!(verbosity{verbosity}: {message}); } } // TODO: Define and implement Filter. 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); }可以看到Loggertrait 是过滤器的统一抽象StderrLogger把消息写到标准错误流main中期望用Filter::new传入一个内部日志器StderrLogger和一个闭包|_verbosity, msg| msg.contains(yikes)随后只有消息文本包含yikes的日志才会被真正输出。这个练习的最大难点在于闭包必须作为字段被存储进结构体而 Rust 中每个闭包的类型都是编译器生成的匿名类型无法直接写出类型名——这正是解答的关键所在。官方解答泛型参数 Fntrait bound完整的官方解答代码位于 exercise.rs标记段solution被 solution.md 通过{{#include exercise.rs:solution}}内联引入核心实现如下/// Only log messages matching a filtering predicate. 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); } } }整个解答可以拆解为三个层次逐层递进地解决在结构体中存储闭包这一核心命题。第一层用泛型参数存储闭包struct FilterL, P { inner: L, predicate: P, }为什么必须用泛型P而不是dyn Fn因为在 Rust 中每一个闭包表达式都会产生一个编译器生成的、唯一的匿名类型——即使是两个写法完全相同的闭包它们的类型也彼此不同。因此没有任何一个具名类型可以容纳任意闭包唯一的选择是让结构体带上类型参数P在实例化时由编译器推断出具体闭包类型。这与inner: L使用泛型约束任意实现了Logger的类型是同一套思路结构体本身不关心具体类型只声明我存储了一个满足某约束的类型。补充P的位置也可以换用Boxdyn Fn(u8, str) - bool这类 trait 对象来实现运行时多态但那样会引入堆分配与动态分派开销。课程解答选择泛型单态化是零开销抽象、编译期多态的代表做法也是性能敏感代码中的首选。第二层用Fntrait bound 声明可调用能力P: Fn(u8, str) - bool,这个 bound 告诉编译器P必须是一个可以被当作函数调用接收一个u8与一个str参数并返回bool的类型。它正是闭包三 trait——Fn对这三者的语义做了精辟总结Fn既不会消耗也不会修改捕获的值调用时只需要对闭包的共享引用因此可以反复调用、甚至可以并发调用FnMut可能会修改捕获的值调用时需要通过独占引用访问闭包对象可以反复调用但不能并发调用FnOnce只能被调用一次调用会消费闭包本身以及所有以move方式捕获的值。三者之间存在子类型关系Fn是FnMut与FnOnce的子类型FnMut又是FnOnce的子类型——也就是说一个Fn闭包可以出现在任何要求FnMut或FnOnce的位置。因此选择 bound 有一条通用建议也写在 traits.md 中定义接收闭包参数的函数时能取FnOnce就取FnOnce只调用一次否则取FnMut最后才取Fn这样能给调用方最大灵活性。而本练习反其道而行之恰好示范了约束该严则严的另一面Logger::log的签名是fn log(self, ...)即通过self共享引用访问过滤器。这意味着在log中只能对predicate做不可变访问。Fn是唯一支持仅凭共享引用即可调用的闭包 trait因此这里的 bound 必须是Fn才能与Loggertrait 的签名自洽。第三层调用字段(self.predicate)(...)if (self.predicate)(verbosity, message) { self.inner.log(verbosity, message); }这里圆括号是必须的self.predicate(...)会被解析为调用名为predicate的方法而(self.predicate)(...)才表示取出字段self.predicate并调用它。这一写法区分了「字段访问 字段调用」与「方法调用」两种语义是 Rust 中调用存储为字段的可调用对象时的标准惯用法。调用流程本身非常直观先执行谓词返回值true表示消息通过过滤随即转发给内部日志器self.inner.log(...)否则直接丢弃。整个Filter对Loggertrait 的实现构成了一个典型的装饰器/中间件模式——在不修改StderrLogger的前提下为日志能力叠加了按谓词过滤这一横切关注点。完整运行效果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); }运行结果只有一行输出到标准错误流verbosity1: yikes, something went wrongFYIverbosity5与uhohverbosity2都被谓词msg.contains(yikes)过滤掉了验证了过滤逻辑的正确性。深入讨论为什么Fn是唯一正确的选择课程在 solution.md 的details教学注释中给出了两个值得细嚼的反证这里展开说明若改用FnMut调用FnMut闭包需要独占引用mut因此log的签名必须变成fn log(mut self, ...)。但这与Loggertrait 定义的fn log(self, ...)直接冲突——要么改 trait影响所有实现方包括StderrLogger要么编译失败。这也解释了为什么 trait 方法签名往往锁死了其内部字段的访问方式。若改用FnOnceFnOnce闭包在调用时会被消费掉按 move 捕获的值随之被吞并。假设log内部是self.predicate(...)一次调用那么整个Filter只能成功记录一条日志消息——第二次调用log时谓词已经不存在了。对于日志器这种被反复调用的抽象来说这是不可接受的。此外课程还特别指出new的impl块同样携带了完整的 bound。从语法上讲where约束并不强制要求写在结构体定义处完全可以只写在真正使用到P的impl块上但把它同时写在new上有一个实际好处帮助类型推断。当调用方写出Filter::new(stderr_logger, closure)时new的签名上有了P: Fn(u8, str) - bool这个目标类型提示编译器可以更顺畅地推断出闭包参数与返回类型避免在main的调用点产生歧义。这也是 Rust 惯用法把约束放在最容易被调用的构造器上改善 API 的可推断性。前置知识串讲闭包语法、捕获与 trait要把上述解答吃透离不开闭包的三个基本概念它们分别对应课程章节中的 syntax.md、capturing.md 与 traits.md。闭包语法|..| ..闭包用竖线创建参数写在|..|之间fn main() { // 轻量写法参数与返回类型均可推断 let double_it |n| n * 2; dbg!(double_it(50)); // 显式写法标注类型并用花括号包裹函数体 let add_1f32 |x: f32| - f32 { x 1.0 }; dbg!(add_1f32(50.)); }要点参数类型可省略由调用点推断返回类型也可省略但只有函数体用{ .. }包裹时才能显式写出。若闭包体是单个表达式花括号可以省略。捕获机制按最宽松的方式捕获闭包可以捕获定义处的环境变量见 capturing.mdfn main() { let max_value 5; let clamp |v| { if v max_value { max_value } else { v } }; dbg!(clamp(1)); dbg!(clamp(3)); dbg!(clamp(7)); }捕获规则是课程强调的重点默认按共享引用捕获如上例max_value被clamp借用main之后仍可访问若闭包内部修改了捕获值则自动升级为按独占引用捕获可以显式加move关键字写作move |v| ..强制按值捕获适用于闭包需要比捕获值活得更久的场景如跨线程、跨生命周期传递编译器总是为每个变量选择**最低需求**的捕获方式优先共享引用其次独占引用最后才 move。这套机制直接决定了闭包实现的到底是Fn、FnMut还是FnOnce只读捕获 →Fn写捕获 →FnMut按 move 消费捕获值 →FnOnce。回到Log Filter练习里的谓词闭包|_verbosity, msg| msg.contains(yikes)不捕获任何外部变量因此它天然实现了完整的Fn能力可以毫无障碍地放进P: Fn(...)的约束中。函数指针与Copy/Clone推断课程还提到一个常被混淆的点fn(..) - T这种特殊类型是函数指针——它要么指向某个具名函数的地址要么是一个不捕获任何东西的闭包这类闭包可以自动强制转换为函数指针。函数指针实现了Copy和Fn。此外编译器会根据闭包捕获的内容自动推断Copy/Clone例如 traits.md 中不捕获的add_suffix是Copy的而捕获了String的take_and_reverse则是Clone的。这些机制共同保证了把闭包存进结构体这一操作在各种使用场景下都足够灵活。小结与延伸练习建议Log Filter解答向我们展示了 Rust 中存储闭包的三板斧问题解法闭包类型无法具名结构体加泛型参数P由编译器推断具体类型无法证明字段可调用施加P: Fn(u8, str) - booltrait bound字段调用与方法调用混淆使用(self.predicate)(...)圆括号语法为什么是Fn而非FnMut/FnOncelog只持有self且日志器需要可反复调用如果你想进一步巩固可以在 playground 上做两个小的变体实验把谓词闭包改成会捕获并修改外部状态如计数的版本观察它是否还能满足P: Fn(...)约束并体会编译器报错信息中指向FnMut的提示尝试为Filter增加一个and_then之类的方法串联多个谓词闭包进一步体会泛型参数 trait bound 的组合能力。本练习位于课程 closures 章节 末尾与其配套的入门素材包括闭包语法 syntax.md、捕获语义 capturing.md 与闭包 trait traits.md官方练习说明见 exercise.md完整可运行源码见 exercise.rs。建议按语法 → 捕获 → trait → 练习的顺序完整阅读即可顺畅理解本文的全部内容。【免费下载链接】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),仅供参考
返回列表