Rust 闭包(Closure)完整详解

Rust 闭包(Closure)完整详解

涵盖 :闭包基础语法、三种捕获方式、Fn/FnMut/FnOnce trait、move 语义等

重点:捕获规则自动判断、闭包与函数指针区别、线程异步场景应用


目录

  1. 闭包是什么
  2. 三种捕获环境的方式(核心重点)
  3. 闭包的三种 trait(Fn / FnMut / FnOnce)(#三闭包的三种 traitfn--fnmut--fnonce)
  4. move 闭包与引用生命周期坑(#四 move-闭包与引用生命周期坑)
  5. 闭包作为返回值
  6. 捕获混合变量:部分 move(#六捕获混合变量部分 move)
  7. [闭包 vs 普通函数指针 fn](#闭包 vs 普通函数指针 fn "#%E4%B8%83%E9%97%AD%E5%8C%85-vs-%E6%99%AE%E9%80%9A%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88-fn")
  8. move 闭包 + 内部可变(结合 RefCell)(#八 move-闭包内部可变结合 refcell)
  9. 高频易错点
  10. 速记口诀

一、闭包是什么

闭包是匿名可执行函数,可以捕获当前作用域的变量,不需要提前命名,常用于回调、迭代器适配器、延迟执行。

语法格式

rust 复制代码
// 基础:|参数 | 表达式
let add = |a, b| a + b;

// 多行用大括号
let calc = |x| {
    let t = x * 2;
    t - 1
};

// 调用
println!("{}", add(1, 2));

对比普通函数

类型 说明
函数 fn add(a:i32,b:i32)->i32 {a+b},不能捕获外部变量
闭包 `

类型推导规则

闭包不强制标注参数/返回类型,编译器自动推导;仅无法推导时才需要手动标注:

rust 复制代码
let f = |x: i32| -> i32 { x + 1 };

二、三种捕获环境的方式(核心重点)

闭包捕获外部变量分三种模式,编译器自动根据使用情况选择:

  1. 不可变借用 &T:只读取变量
  2. 可变借用 &mut T:修改变量
  3. 获取所有权 move:转移变量到闭包内

1. 不可变借用捕获(只读)

只读取外部变量,不修改、不转移所有权,原变量仍可用:

rust 复制代码
let s = String::from("hello");
let read = || println!("{}", s); // 捕获 &String
read();
println!("{}", s); // s 正常可用

2. 可变借用捕获(修改)

闭包内部修改外部变量,外部变量必须 mut

rust 复制代码
let mut num = 0;
let mut inc = || num += 1; // &mut num
inc();
println!("{}", num); // 1

3. move 强制获取所有权

关键字 move:强制把捕获变量的所有权转移进闭包,外部变量失效。

适用场景:闭包需要长期存活(线程、异步),生命周期长于原变量。

rust 复制代码
let s = String::from("data");
let consume = move || println!("{}", s); // s 所有权移入闭包
consume();
// println!("{}", s); // 编译报错,s 已 move
典型场景:线程

线程生命周期独立,必须 move 把数据移入闭包:

rust 复制代码
use std::thread;
let msg = String::from("thread msg");
let handle = thread::spawn(move || {
    println!("{}", msg);
});
handle.join().unwrap();

捕获规则自动判断总结

使用方式 捕获模式
仅读 &T 不可变借用
内部修改 &mut T 可变借用
使用 move 关键字 强制转移所有权
同时读 + 修改 可变借用

三、闭包的三种 trait(Fn / FnMut / FnOnce)

所有闭包自动实现以下其中一个或多个 trait,区分依据是捕获方式、调用次数

1. FnOnce

特性 说明
调用次数 只能调用一次
捕获方式 获取所有权 move,内部消耗变量(如 String)
行为 调用后内部资源被 drop,无法二次执行

2. FnMut

特性 说明
调用次数 可调用多次
捕获方式 &mut T
要求 闭包变量本身需要声明 mut

3. Fn

特性 说明
调用次数 可无限次调用
捕获方式 &T,无任何修改
行为 只读取捕获变量,不修改、不获取所有权

优先级兼容关系

rust 复制代码
Fn ⊂ FnMut ⊂ FnOnce
  • Fn 自动实现 FnMut + FnOnce
  • FnMut 自动实现 FnOnce

函数参数约束闭包类型

rust 复制代码
// 接收只读闭包 Fn
fn run_fn<F: Fn()>(f: F) { f(); f(); }

// 接收可修改捕获的闭包 FnMut
fn run_mut<F: FnMut()>(mut f: F) { f(); f(); }

// 仅能执行一次的闭包 FnOnce
fn run_once<F: FnOnce()>(f: F) { f(); }

迭代器适配器全部接收 Fn / FnMut

rust 复制代码
let nums = vec![1, 2, 3];
// map 接收 Fn
let double: Vec<_> = nums.iter().map(|x| x * 2).collect();
// filter 接收 Fn
let even: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// fold 累加闭包 FnMut
let sum = nums.iter().fold(0, |acc, &x| acc + x);

四、move 闭包与引用生命周期坑

  1. 不加 move 捕获引用:闭包生命周期绑定外部变量,外部销毁则闭包失效;
  2. move 捕获所有权:数据归属闭包,不受外部生命周期限制,适合跨线程;
  3. move 不会强制拷贝,只是转移所有权(Move 语义);如果捕获 Copy 类型(i32),会直接复制一份值。
rust 复制代码
// Copy 类型 move 只是拷贝,外部变量仍可用
let a = 10;
let f = move || println!("{}", a);
f();
println!("{}", a); // 正常,i32 实现 Copy

五、闭包作为返回值

方案 1:impl Trait(静态分发,单一闭包类型)

函数只能返回同一种闭包,性能无开销:

rust 复制代码
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8

方案 2:Box<dyn Fn()>(动态分发,支持多分支返回不同闭包)

闭包尺寸不固定,装箱放入堆,胖指针返回:

rust 复制代码
fn select_fun(flag: bool) -> Box<dyn Fn(i32) -> i32> {
    if flag {
        Box::new(|x| x * 2)
    } else {
        Box::new(|x| x / 2)
    }
}

两种方案对比

方案 说明
impl Trait 静态分发,单一闭包类型,性能无开销
Box 动态分发,支持多分支返回不同闭包,有运行时开销

六、捕获混合变量:部分 move

Rust 闭包捕获是粒度为整个变量,无法只 move 结构体某个字段。

如果只想转移部分字段,手动解构提前移动:

rust 复制代码
struct Data {
    name: String,
    id: i32,
}
let d = Data { name: "test".into(), id: 1 };
// 提前把 name 移出去,闭包只 move name,id 借用
let name = d.name;
let f = move || println!("{}", name);

七、闭包 vs 普通函数指针 fn

1. fn 函数指针

特性 说明
捕获能力 不能捕获任何外部环境,无上下文
类型写法 fn(i32)->i32
存储形式 轻量,仅地址

2. 闭包

特性 说明
捕获能力 可捕获环境变量,携带上下文
类型写法 无法直接写成简单 fn 类型,只能用 impl Fn / Box<dyn Fn>
底层实现 匿名结构体:存储所有捕获的变量

相互转换:无捕获闭包可转为 fn

不捕获任何外部变量的闭包,能自动转函数指针:

rust 复制代码
let f = |a: i32| a + 1;
let fp: fn(i32) -> i32 = f;

八、move 闭包 + 内部可变(结合 RefCell)

move 拿走所有权,但需要内部修改,配合 RefCell 内部可变性:

rust 复制代码
use std::cell::RefCell;
use std::rc::Rc;

let cnt = Rc::new(RefCell::new(0));
let c = Rc::clone(&cnt);
let f = move || {
    *c.borrow_mut() += 1;
};
f();
println!("{}", cnt.borrow());

多线程场景 :替换为 Arc<Mutex<T>>


九、高频易错点

⚠️ 以下易错点需特别注意

  1. 迭代器传入修改外部变量的闭包 ,闭包必须 mut
  2. 不加 move 的闭包无法传入线程,生命周期不匹配编译报错;
  3. 返回多个不同闭包不能用 impl Trait ,必须 Box<dyn Fn>
  4. 闭包捕获是整变量捕获,不能单独捕获结构体某个成员;
  5. move 对 Copy 类型是复制,非 Copy 类型是所有权转移;
  6. FnOnce 闭包只能调用一次,第二次调用编译报错;
  7. 闭包自动推导类型,复杂场景类型推导失败需手动标注参数类型。

十、速记口诀

rust 复制代码
闭包匿名函数,竖线包裹参数;
自动捕获外部变量,三种借用模式自动选;
move 强制转移所有权,线程异步必备;
Fn 只读可多次,FnMut 可修改捕获;
FnOnce 仅执行一次,三者包含关系记心间;
impl Trait 静态返回单一闭包,Box dyn 支持多分支;
无捕获闭包可转 fn 指针,捕获环境不能转;
迭代器 map/filter 全用闭包,延迟执行简洁高效。
相关推荐
ServBay1 小时前
如何利用本地技术栈构建 0 成本 AI SaaS 雏形
后端·aigc·ai编程
菜鸟谢1 小时前
Rust 集合 + 迭代器完整详解
后端
杨利杰YJlio1 小时前
Codex桌面客户端上手:项目、插件与自动化实战
前端·后端
常铭1 小时前
【Java基础】01-HashMap的底层原理
后端·面试
幼儿园技术家2 小时前
实现 GEO 监控:从多引擎探测到优化闭环
前端·后端
掘金者阿豪2 小时前
微信小程序虚拟支付与广告转化回传实战记录
后端
ping某3 小时前
专栏-null 和 undefined 到底是什么?
前端·javascript·后端
神奇小汤圆3 小时前
别再只会用ArrayList了!Java集合框架的性能天花板到底在哪?
后端
神奇小汤圆3 小时前
Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
后端