7. Closures and Higher-Order Functions 🟢
# 7. 闭包与高阶函数 🟢
What you’ll learn:
本章将学到什么:
- The three closure traits (
Fn,FnMut,FnOnce) and how capture works
三个闭包 trait:Fn、FnMut、FnOnce,以及捕获机制如何运作- Passing closures as parameters and returning them from functions
如何把闭包当参数传递,以及如何从函数里返回闭包- Combinator chains and iterator adapters for functional-style programming
函数式风格里的组合器链和迭代器适配器- Designing your own higher-order APIs with the right trait bounds
如何给自己的高阶 API 选出合适的 trait 约束
Fn, FnMut, FnOnce — The Closure Traits
Fn、FnMut、FnOnce:闭包的三个 Trait
Every closure in Rust implements one or more of three traits, based on how it captures variables:
Rust 里的每个闭包,都会根据它捕获变量的方式,实现这三个 trait 里的一个或多个:
#![allow(unused)]
fn main() {
// FnOnce — consumes captured values (can only be called once)
let name = String::from("Alice");
let greet = move || {
println!("Hello, {name}!"); // Takes ownership of `name`
drop(name); // name is consumed
};
greet(); // ✅ First call
// greet(); // ❌ Can't call again — `name` was consumed
// FnMut — mutably borrows captured values (can be called many times)
let mut count = 0;
let mut increment = || {
count += 1; // Mutably borrows `count`
};
increment(); // count == 1
increment(); // count == 2
// Fn — immutably borrows captured values (can be called many times, concurrently)
let prefix = "Result";
let display = |x: i32| {
println!("{prefix}: {x}"); // Immutably borrows `prefix`
};
display(1);
display(2);
}
The hierarchy: Fn : FnMut : FnOnce — each is a subtrait of the next:
层级关系:Fn : FnMut : FnOnce,前者是后者的子 trait:
FnOnce ← everything can be called at least once
↑
FnMut ← can be called repeatedly (may mutate state)
↑
Fn ← can be called repeatedly and concurrently (no mutation)
If a closure implements Fn, it also implements FnMut and FnOnce.
如果一个闭包实现了 Fn,那它也一定同时实现 FnMut 和 FnOnce。
Closures as Parameters and Return Values
把闭包作为参数和返回值
// --- Parameters ---
// Static dispatch (monomorphized — fastest)
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
// Also written with impl Trait:
fn apply_twice_v2(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// Dynamic dispatch (trait object — flexible, slight overhead)
fn apply_dyn(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// --- Return Values ---
// Can't return closures by value without boxing (they have anonymous types):
fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + n)
}
// With impl Trait (simpler, monomorphized, but can't be dynamic):
fn make_adder_v2(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let double = |x: i32| x * 2;
println!("{}", apply_twice(double, 3)); // 12
let add5 = make_adder(5);
println!("{}", add5(10)); // 15
}
The main trade-off is the usual Rust one: monomorphized generics are fastest and most optimizable, while trait objects are more flexible when you need dynamic behavior or heterogeneous storage.
这里的主要取舍还是 Rust 里那套老规律:单态化泛型最快、最容易被优化;trait object 更灵活,适合需要动态行为或异构存储的场景。
Combinator Chains and Iterator Adapters
组合器链与迭代器适配器
Higher-order functions shine with iterators — this is idiomatic Rust:
高阶函数和迭代器组合在一起时特别顺手,这也是非常典型的 Rust 写法:
#![allow(unused)]
fn main() {
// C-style loop (imperative):
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut result = Vec::new();
for x in &data {
if x % 2 == 0 {
result.push(x * x);
}
}
// Idiomatic Rust (functional combinator chain):
let result: Vec<i32> = data.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
// Same performance — iterators are lazy and optimized by LLVM
assert_eq!(result, vec![4, 16, 36, 64, 100]);
}
Common combinators cheat sheet:
常见组合器速查:
| Combinator 组合器 | What It Does 作用 | Example 示例 |
|---|---|---|
.map(f) | Transform each element 变换每个元素 | `.map( |
.filter(p) | Keep elements where predicate is true 保留满足条件的元素 | `.filter( |
.filter_map(f) | Map + filter in one step (returns Option)一步完成映射与过滤,返回 Option | `.filter_map( |
.flat_map(f) | Map then flatten nested iterators 映射后再拍平嵌套迭代器 | `.flat_map( |
.fold(init, f) | Reduce to single value 归约成单个值 | `.fold(0, |
.any(p) / .all(p) | Short-circuit boolean check 短路布尔判断 | `.any( |
.enumerate() | Add index 附带索引 | `.enumerate().map( |
.zip(other) | Pair with another iterator 与另一个迭代器配对 | .zip(labels.iter()) |
.take(n) / .skip(n) | First/skip N elements 取前 N 个或跳过前 N 个 | .take(10) |
.chain(other) | Concatenate two iterators 连接两个迭代器 | .chain(extra.iter()) |
.peekable() | Look ahead without consuming 提前查看下一个元素而不消费 | .peek() |
.collect() | Gather into a collection 收集进集合 | .collect::<Vec<_>>() |
Implementing Your Own Higher-Order APIs
自己设计高阶 API
Design APIs that accept closures for customization:
可以把闭包作为可配置逻辑的一部分塞进 API 里:
#![allow(unused)]
fn main() {
/// Retry an operation with a configurable strategy
fn retry<T, E, F, S>(
mut operation: F,
mut should_retry: S,
max_attempts: usize,
) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
S: FnMut(&E, usize) -> bool, // (error, attempt) → try again?
{
for attempt in 1..=max_attempts {
match operation() {
Ok(val) => return Ok(val),
Err(e) if attempt < max_attempts && should_retry(&e, attempt) => {
continue;
}
Err(e) => return Err(e),
}
}
unreachable!()
}
// Usage — caller controls retry logic:
}
#![allow(unused)]
fn main() {
fn connect_to_database() -> Result<(), String> { Ok(()) }
fn http_get(_url: &str) -> Result<String, String> { Ok(String::new()) }
trait TransientError { fn is_transient(&self) -> bool; }
impl TransientError for String { fn is_transient(&self) -> bool { true } }
let url = "http://example.com";
let result = retry(
|| connect_to_database(),
|err, attempt| {
eprintln!("Attempt {attempt} failed: {err}");
true // Always retry
},
3,
);
// Usage — retry only specific errors:
let result = retry(
|| http_get(url),
|err, _| err.is_transient(), // Only retry transient errors
5,
);
}
This style is powerful because the framework owns the control flow, while the caller injects just the variable behavior. It is one of the cleanest ways to build reusable policy-driven APIs in Rust.
这种写法厉害的地方在于:控制流程由框架统一掌握,调用方只注入变化的那部分策略。拿它来做“可复用但可定制”的策略型 API,很顺手。
The with Pattern — Bracketed Resource Access
with 模式:成对括起来的资源访问
Sometimes a resource must be placed into a specific state for the duration of one operation and restored afterwards, even if the caller returns early or errors out. Instead of exposing the raw resource and hoping the caller remembers setup and teardown, a with_* API lends the resource through a closure:
有时候一个资源必须先被设置到特定状态,执行完操作后再恢复回来,而且哪怕调用方中途返回、? 提前退出也一样要恢复。这时候与其把原始资源裸露给调用方、赌对方记得前后收尾,不如用 with_* 这种 API,通过闭包把资源“借”出去:
set up → call closure with resource → tear down
The caller never manages setup or teardown directly, so forgetting either side becomes impossible.
这样一来,调用方根本碰不到 setup 和 teardown 本身,也就谈不上“忘了做其中一步”。
Example: GPIO Pin Direction
例子:GPIO 引脚方向
A GPIO controller manages pins that support bidirectional I/O. Some callers need input mode, others need output mode. Instead of exposing raw pin access and trusting callers to set direction correctly, the controller provides with_pin_input and with_pin_output:
GPIO 控制器里的引脚可能既能输入也能输出。有的调用方需要输入模式,有的需要输出模式。与其把底层引脚访问和方向设置全都暴露出去,不如直接给出 with_pin_input 和 with_pin_output 两套接口:
#![allow(unused)]
fn main() {
/// GPIO pin direction — not public, callers never set this directly.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction { In, Out }
/// A GPIO pin handle lent to the closure. Cannot be stored or cloned —
/// it exists only for the duration of the callback.
pub struct GpioPin<'a> {
pin_number: u8,
_controller: &'a GpioController,
}
impl GpioPin<'_> {
pub fn read(&self) -> bool {
// Read pin level from hardware register
println!(" reading pin {}", self.pin_number);
true // stub
}
pub fn write(&self, high: bool) {
// Drive pin level via hardware register
println!(" writing pin {} = {high}", self.pin_number);
}
}
pub struct GpioController {
current_direction: std::cell::Cell<Option<Direction>>,
}
impl GpioController {
pub fn new() -> Self {
GpioController {
current_direction: std::cell::Cell::new(None),
}
}
pub fn with_pin_input<R>(
&self,
pin: u8,
mut f: impl FnMut(&GpioPin<'_>) -> R,
) -> R {
let prev = self.current_direction.get();
self.set_direction(pin, Direction::In);
let handle = GpioPin { pin_number: pin, _controller: self };
let result = f(&handle);
if let Some(dir) = prev {
self.set_direction(pin, dir);
}
result
}
pub fn with_pin_output<R>(
&self,
pin: u8,
mut f: impl FnMut(&GpioPin<'_>) -> R,
) -> R {
let prev = self.current_direction.get();
self.set_direction(pin, Direction::Out);
let handle = GpioPin { pin_number: pin, _controller: self };
let result = f(&handle);
if let Some(dir) = prev {
self.set_direction(pin, dir);
}
result
}
fn set_direction(&self, pin: u8, dir: Direction) {
println!(" [hw] pin {pin} → {dir:?}");
self.current_direction.set(Some(dir));
}
}
}
What the with pattern guarantees:with 模式保证了什么:
- Direction is always set before the caller’s code runs
调用方代码运行前,引脚方向一定已经设置好 - Direction is always restored after, even if the closure returns early
闭包执行结束后方向一定会恢复,即使中途提前返回也一样 - The
GpioPinhandle cannot escape the closureGpioPin句柄无法逃出闭包作用域 - Callers never import
Direction, never callset_direction
调用方不需要接触Direction,也碰不到set_direction
Where This Pattern Appears
这个模式通常出现在哪些地方
| API | Setup 准备阶段 | Callback 回调阶段 | Teardown 收尾阶段 |
|---|---|---|---|
std::thread::scope | Create scope 创建作用域 | |s| { s.spawn(...) } | Join all threads 等待所有线程结束 |
Mutex::lock | Acquire lock 拿到锁 | Use MutexGuard | Release on drop 离开作用域自动释放 |
tempfile::tempdir | Create temp directory 创建临时目录 | Use path 使用路径 | Delete on drop 离开时删除 |
std::io::BufWriter::new | Buffer writes 建立缓冲写入 | Write operations 执行写入 | Flush on drop 释放时刷新 |
GPIO with_pin_* | Set direction 设置方向 | Use pin handle 使用引脚句柄 | Restore direction 恢复方向 |
withvs RAII (Drop): Both ensure cleanup. Use RAII orDropwhen the caller needs to hold the resource across multiple statements or function calls. Usewithwhen the operation is tightly bracketed and the caller should not be able to break that bracket.with和 RAII 的区别:两者都能保证收尾。调用方如果需要跨多个语句、多个函数长期持有资源,适合 RAII 或Drop;如果整个操作天然就是“一次准备、一段工作、一次收尾”,而且不希望调用方打破这个边界,就更适合with。
FnMutvsFnin API design:FnMutis usually the default bound because callers can pass eitherFnorFnMut. Only requireFnif the closure may be called concurrently, and only requireFnOnceif the callback is consumed by a single call.
API 设计里FnMut和Fn怎么选:FnMut往往是默认选择,因为它既能接Fn闭包,也能接会修改捕获状态的FnMut闭包。只有在闭包可能被并发调用时,才需要把约束抬到Fn;只有确定只调用一次时,才收紧到FnOnce。
Key Takeaways — Closures
本章要点 — 闭包
Fndoes shared borrowing,FnMutdoes mutable borrowing,FnOnceconsumes captures; accept the weakest bound your API needsFn做共享借用,FnMut做可变借用,FnOnce会消费捕获值;API 设计时尽量接受“最弱但够用”的约束impl Fnis great for parameters and returns;Box<dyn Fn>is for dynamic storage
参数和返回值里经常适合impl Fn;需要动态存储时再用Box<dyn Fn>- Combinator chains compose cleanly and often optimize into tight loops
组合器链写起来很整洁,而且通常会被优化成很紧凑的循环- The
withpattern guarantees setup/teardown and prevents resource escapewith模式可以把准备与收尾强行绑死,还能阻止资源逃逸
See also: Ch 2 — Traits In Depth, Ch 8 — Functional vs. Imperative, and Ch 15 — API Design.
延伸阅读: 相关内容还可以继续看 第 2 章:Trait 深入解析、第 8 章:函数式与命令式 和 第 15 章:API 设计。
graph TD
FnOnce["FnOnce<br/>(can call once)<br/>只能调用一次"]
FnMut["FnMut<br/>(can call many times,<br/>may mutate captures)<br/>可多次调用,可能修改捕获值"]
Fn["Fn<br/>(can call many times,<br/>immutable captures)<br/>可多次调用,只做共享借用"]
Fn -->|"implements<br/>同时实现"| FnMut
FnMut -->|"implements<br/>同时实现"| FnOnce
style Fn fill:#d4efdf,stroke:#27ae60,color:#000
style FnMut fill:#fef9e7,stroke:#f1c40f,color:#000
style FnOnce fill:#fadbd8,stroke:#e74c3c,color:#000
Every
Fnis alsoFnMut, and everyFnMutis alsoFnOnce. AcceptFnMutby default — it is usually the most flexible bound for callers.
每个Fn也都是FnMut,每个FnMut也都是FnOnce。大多数时候,默认接受FnMut是最灵活的做法。
Exercise: Higher-Order Combinator Pipeline ★★ (~25 min)
练习:高阶组合器流水线 ★★(约 25 分钟)
Create a Pipeline struct that chains transformations. It should support .pipe(f) to add a transformation and .execute(input) to run the full chain.
实现一个 Pipeline 结构体,用来串联多个变换步骤。它需要支持 .pipe(f) 添加变换函数,并通过 .execute(input) 运行整条流水线。
🔑 Solution
🔑 参考答案
struct Pipeline<T> {
transforms: Vec<Box<dyn Fn(T) -> T>>,
}
impl<T: 'static> Pipeline<T> {
fn new() -> Self {
Pipeline { transforms: Vec::new() }
}
fn pipe(mut self, f: impl Fn(T) -> T + 'static) -> Self {
self.transforms.push(Box::new(f));
self
}
fn execute(self, input: T) -> T {
self.transforms.into_iter().fold(input, |val, f| f(val))
}
}
fn main() {
let result = Pipeline::new()
.pipe(|s: String| s.trim().to_string())
.pipe(|s| s.to_uppercase())
.pipe(|s| format!(">>> {s} <<<"))
.execute(" hello world ".to_string());
println!("{result}"); // >>> HELLO WORLD <<<
let result = Pipeline::new()
.pipe(|x: i32| x * 2)
.pipe(|x| x + 10)
.pipe(|x| x * x)
.execute(5);
println!("{result}"); // (5*2 + 10)^2 = 400
}