C 里写回调,最常见的形状是函数指针加上下文指针:
typedef void (*event_cb_t)(void *ctx, int event);
void register_event(event_cb_t cb, void *ctx);
cb 是要调用的函数,ctx 是调用时带回来的环境。这个模式很成熟,但也有老问题:
ctx的真实类型靠约定ctx活多久靠约定- 回调会不会修改环境靠约定
- 回调会不会只调用一次靠约定
- 多线程调用时是否安全靠约定
Rust 的 closure 解决的是同一个问题:把“要执行的代码”和“它需要的环境”放在一起。
let prefix = String::from("event");
let callback = |event: i32| {
println!("{prefix}: {event}");
};
callback 不只是函数。它还捕获了外部变量 prefix。
closure 是带环境的函数
普通函数不能捕获局部变量:
fn print_event(event: i32) {
println!("{event}");
}
closure 可以:
let name = String::from("sensor");
let print_event = |event: i32| {
println!("{name}: {event}");
};
print_event(1);
从 C 角度看,可以把它想成:
closure = 函数指针 + 编译器生成的 ctx
区别是,Rust 知道这个 ctx 里有什么,也知道它是只读借用、可变借用,还是被拿走所有权。
Fn:只读捕获,可以调用多次
如果 closure 只是读取捕获的变量,它通常实现 Fn:
fn call_twice<F>(callback: F)
where
F: Fn(i32),
{
callback(1);
callback(2);
}
fn main() {
let name = String::from("sensor");
call_twice(|event| {
println!("{name}: {event}");
});
}
Fn(i32) 表示这个回调可以用只读方式调用。它可以调用多次,不需要修改自身捕获的环境。
这类似 C 里回调只读 ctx:
void on_event(void *ctx, int event)
{
const struct Config *config = ctx;
printf("%s %d\n", config->name, event);
}
Rust 把“只读使用环境”写进了 closure trait。
FnMut:回调会修改捕获环境
如果 closure 要修改捕获的变量,它通常实现 FnMut:
fn call_events<F>(mut callback: F)
where
F: FnMut(i32),
{
callback(1);
callback(2);
}
fn main() {
let mut count = 0;
call_events(|event| {
count += 1;
println!("event {event}, count {count}");
});
}
callback 参数前要写 mut,因为调用 FnMut 可能会修改 closure 自己保存的环境。
C 里也常这样写:
struct Counter {
int count;
};
void on_event(void *ctx, int event)
{
struct Counter *counter = ctx;
counter->count++;
}
Rust 的区别在于:可变捕获会受借用规则约束。只要这个 closure 还持有对 count 的可变借用,你就不能在外面同时乱改 count。
FnOnce:回调会消耗捕获的值
如果 closure 把捕获的值移动出去,它只能调用一次,通常实现 FnOnce:
fn call_once<F>(callback: F)
where
F: FnOnce(),
{
callback();
}
fn main() {
let message = String::from("done");
call_once(|| {
println!("{message}");
drop(message);
});
}
message 被移动进 closure,并在调用时被消耗。这个 closure 不能再调用第二次。
从 C 角度看,这类似“一次性回调接管 ctx,并负责释放”。不同的是,Rust 会在类型层面表达“只能调用一次”。
move closure 拿走捕获值
默认情况下,closure 会按需要借用或移动外部变量。你也可以显式写 move,让 closure 拿走捕获值:
let name = String::from("worker");
let callback = move || {
println!("{name}");
};
move 常见于线程、异步任务或需要把回调保存起来的场景:
let path = String::from("data.txt");
std::thread::spawn(move || {
println!("process {path}");
});
新线程可能比当前函数活得更久,所以不能借用当前栈上的局部变量。move 把 path 的所有权交给线程里的 closure。
这和 C 里把 ctx 指向栈变量再异步回调非常不同。Rust 会阻止这种悬垂引用。
函数指针和 closure 不完全一样
Rust 也有函数指针:
fn handle(event: i32) {
println!("{event}");
}
let f: fn(i32) = handle;
函数指针不捕获环境。它更接近 C 的纯函数指针。
不捕获环境的 closure 也可以转换成函数指针:
let f: fn(i32) = |event| println!("{event}");
但捕获环境的 closure 不行:
let prefix = String::from("event");
// let f: fn(i32) = |event| println!("{prefix}: {event}"); // 不允许
因为普通函数指针没有地方存 prefix。需要保存环境时,用泛型参数 F: Fn...,或者用 trait object。
库 API 怎样接收回调
如果函数只是立即调用回调,可以用泛型:
fn for_each_event<F>(events: &[i32], mut callback: F)
where
F: FnMut(i32),
{
for event in events {
callback(*event);
}
}
调用方可以传函数:
fn print_event(event: i32) {
println!("{event}");
}
for_each_event(&[1, 2, 3], print_event);
也可以传 closure:
let mut sum = 0;
for_each_event(&[1, 2, 3], |event| {
sum += event;
});
为什么这里用 FnMut?因为它覆盖面更实用:既能接收只读 closure,也能接收会修改捕获状态的 closure。
保存回调时通常需要 Box
如果结构体要长期保存回调,不能只写泛型函数参数。常见写法是 Box<dyn FnMut(...)>:
struct Dispatcher {
callback: Box<dyn FnMut(i32)>,
}
impl Dispatcher {
fn new<F>(callback: F) -> Self
where
F: FnMut(i32) + 'static,
{
Self {
callback: Box::new(callback),
}
}
fn dispatch(&mut self, event: i32) {
(self.callback)(event);
}
}
Box<dyn FnMut(i32)> 可以先理解成:
堆上的 closure 环境 + 一张调用表
'static 不是说回调永远不释放,而是说这个 closure 不能借用即将失效的局部变量。它要么拥有捕获的数据,要么只捕获真正能活足够久的引用。
FFI 回调仍然要回到 C 形状
Rust closure 很适合 Rust 内部 API。但跨 C ABI 时,不能直接把捕获环境的 closure 当成 C 函数指针传出去。
C 需要的仍然通常是:
typedef void (*event_cb_t)(void *ctx, int event);
Rust 边界要用 extern "C" 函数和明确的 context 指针。常见做法是把 closure 装进一个拥有类型,再在 FFI trampoline 里取出来调用。
这部分涉及裸指针、生命周期、线程和释放责任,后面的 FFI 回调专题再展开。现在先记住:Rust 内部用 closure,C 边界用明确 ABI 的函数指针和 ctx。
这一篇先记住什么
C 和 Rust 的第一层对应关系是:
函数指针 -> fn(...) 或不捕获 closure
函数指针 + void *ctx -> closure
只读使用 ctx -> Fn
修改 ctx -> FnMut
接管 ctx / 只能调用一次 -> FnOnce
异步保存或跨线程执行 -> 常见 move closure
长期保存回调 -> Box<dyn FnMut(...)>
C ABI 回调 -> extern "C" fn + ctx,不直接传捕获 closure
closure 的重点不是少写一个 ctx 参数,而是让“代码需要哪些环境、怎样使用这些环境、能调用几次”进入类型系统。
下一篇讲并发:Send、Sync、Arc 和 Mutex。C 里线程安全主要靠约定和锁纪律,Rust 会把很多跨线程移动和共享规则写进类型。