第 5 章:捕获 — 一个函数不只是代码块
一个让你睡不着觉的闭包问题
先看这段代码。它看起来无害:
def make_discount_calculator(discount_rate):
def apply(order):
return order.amount * discount_rate
return apply然后你写了:
calculators = []
for rate in [0.1, 0.2, 0.3]:
calculators.append(make_discount_calculator(rate))这段代码没问题。但如果是这样呢:
calculators = []
for rate in [0.1, 0.2, 0.3]:
calculators.append(lambda order: order.amount * rate)
# 三个 lambda 都用了最后一个 rate(0.3)!
# 因为它们都捕获的是同一个变量 rate,而不是创建时的值闭包看起来像一个小特性——几百行代码里出现一两次。但它的微妙之处足以让有经验的程序员在凌晨盯着日志怀疑人生。
一个函数只是要执行的任务吗?不。它还在你不知不觉间,带走了一整片上下文。
5.1 语法基础:8 种语言如何定义和组合行为
Python
# 普通函数
def calculate_discount(order: Order, rate: float) -> float:
return order.amount * rate
# 默认参数
def apply_discount(order: Order, rate: float = 0.1) -> float:
return order.amount * (1 - rate)
# lambda(单表达式匿名函数)
discount = lambda o: o.amount * 0.9
# 高阶函数
def apply_to_orders(orders: list[Order], f) -> list[float]:
return [f(o) for o in orders]
# 闭包
def make_multiplier(factor: float):
return lambda x: x * factor # 捕获 factor
# 使用
doubler = make_multiplier(2.0)
doubler(5.0) # 10.0JavaScript / TypeScript
// 函数声明
function calculateDiscount(order: Order, rate: number): number {
return order.amount * rate;
}
// 箭头函数
const discount = (order: Order, rate: number = 0.1): number =>
order.amount * (1 - rate);
// 高阶函数
const applyToOrders = (orders: Order[], f: (o: Order) => number): number[] =>
orders.map(f);
// 闭包:箭头函数捕获 this 的行为和 function 完全不同
function createCounter() {
let count = 0;
return {
increment: () => ++count, // 箭头函数不绑定自己的 this
get: function() { return count; }
};
}Java
// 方法
public double calculateDiscount(Order order, double rate) {
return order.amount() * rate;
}
// Lambda(Java 8+)
Function<Order, Double> discount = o -> o.amount() * 0.9;
// 方法引用
Function<Order, Double> getAmount = Order::amount;
// 高阶函数
public List<Double> applyToOrders(List<Order> orders, Function<Order, Double> f) {
return orders.stream().map(f).toList();
}
// 闭包:Java lambda 只能捕获 effectively final 的变量
double rate = 0.1;
Function<Order, Double> apply = o -> o.amount() * rate;
// rate = 0.2; // 如果取消注释,上面的 lambda 编译错误C++
// 函数
double calculate_discount(const Order& order, double rate) {
return order.amount * rate;
}
// Lambda(C++11+)
auto discount = [](const Order& o) { return o.amount * 0.9; };
// Lambda 捕获:显式声明是值捕获还是引用捕获
double rate = 0.1;
auto apply = [rate](const Order& o) { return o.amount * rate; }; // 值捕获
auto apply_ref = [&rate](const Order& o) { return o.amount * rate; }; // 引用捕获
auto apply_all = [=](const Order& o) { return o.amount * rate; }; // 全部值捕获
auto apply_all_ref = [&](const Order& o) { return o.amount * rate; }; // 全部引用捕获
// std::function:可存储任何可调用对象
std::function<double(const Order&)> fn = discount;
// 高阶函数
std::vector<double> apply_to_orders(
const std::vector<Order>& orders,
const std::function<double(const Order&)>& f
);Rust
// 函数
fn calculate_discount(order: &Order, rate: f64) -> f64 {
order.amount * rate
}
// 闭包
let discount = |o: &Order| o.amount * 0.9;
// 闭包的三种 trait:Fn(不可变借用)、FnMut(可变借用)、FnOnce(获取所有权)
let rate = 0.1;
let apply = |o: &Order| o.amount * rate; // Fn:只读捕获
let mut total = 0.0;
let mut accum = |o: &Order| total += o.amount; // FnMut:可变捕获
let orders_vec = vec![order];
let consume = || { let _v = orders_vec; }; // FnOnce:move 捕获
// 高阶函数
fn apply_to_orders(orders: &[Order], f: impl Fn(&Order) -> f64) -> Vec<f64> {
orders.iter().map(f).collect()
}Go
// 函数
func calculateDiscount(order Order, rate float64) float64 {
return order.Amount * rate
}
// Go 没有 lambda 语法,但有匿名函数
discount := func(o Order) float64 {
return o.Amount * 0.9
}
// 闭包:Go 的闭包自动捕获引用
func makeMultiplier(factor float64) func(float64) float64 {
return func(x float64) float64 {
return x * factor // 捕获 factor 的引用
}
}
// 高阶函数
func applyToOrders(orders []Order, f func(Order) float64) []float64 {
result := make([]float64, len(orders))
for i, o := range orders {
result[i] = f(o)
}
return result
}Swift
// 函数
func calculateDiscount(order: Order, rate: Double) -> Double {
return order.amount * rate
}
// 闭包
let discount: (Order) -> Double = { $0.amount * 0.9 }
// 尾随闭包语法
let paidOrders = orders.filter { $0.paid }
// 闭包捕获列表:显式控制捕获语义
func loadData(completion: @escaping () -> Void) {
DispatchQueue.global().async { [weak self] in
// weak self 避免循环引用
self?.updateUI()
}
}
// 高阶函数
func applyToOrders(_ orders: [Order], _ f: (Order) -> Double) -> [Double] {
return orders.map(f)
}Kotlin
// 函数
fun calculateDiscount(order: Order, rate: Double): Double {
return order.amount * rate
}
// Lambda
val discount: (Order) -> Double = { it.amount * 0.9 }
// 尾随 lambda
val paidOrders = orders.filter { it.paid }
// 带接收者的 lambda(Kotlin 特有)
fun buildOrder(block: OrderBuilder.() -> Unit): Order {
val builder = OrderBuilder()
builder.block()
return builder.build()
}
// 使用
val order = buildOrder {
setId("A1001")
setAmount(99.0)
}
// 高阶函数
fun applyToOrders(orders: List<Order>, f: (Order) -> Double): List<Double> {
return orders.map(f)
}语法速查:函数与闭包
| 能力 | Python | TypeScript | Java | C++ | Rust | Go | Swift | Kotlin |
|---|---|---|---|---|---|---|---|---|
| lambda/匿名函数 | lambda x: expr | x => expr | x -> expr | [](auto x){...} | |x| expr | func(x T) T {...} | { $0 ... } | { it ... } |
| 闭包捕获 | 引用捕获 | 引用捕获 | effectively final 值 | 显式控制 [=] / [&] | 按 trait 区分 Fn/FnMut/FnOnce | 引用捕获 | 默认强引用,可用 [weak self] | 引用捕获 |
| 函数类型写法 | Callable[[In], Out] | (x: In) => Out | Function<In, Out> | std::function<Out(In)> | fn(In) -> Out | func(In) Out | (In) -> Out | (In) -> Out |
| 高阶函数 | map(f) | map(f) | stream().map(f) | std::transform | iter().map(f) | for 循环 | map(f) | map(f) |
| 尾随闭包语法 | 无 | 无 | 无 | 无 | 无 | 无 | ✓ | ✓ |
| 带接收者的 lambda | 无 | 无 | 无 | 无 | 无 | 无 | 无 | ✓ |
5.2 深度对照一:闭包捕获了什么——不是所有语言都一样
这是跨语言迁移中最容易被忽视、却最容易出问题的差异。
引用捕获 vs 值捕获
大多数语言默认引用捕获:闭包里用到的外部变量,捕获的是变量的引用(或指针),而不是创建时的值。
# Python:默认引用捕获
funcs = []
for i in range(3):
funcs.append(lambda: i) # 捕获变量 i 的引用
print([f() for f in funcs]) # [2, 2, 2] —— 全是最后一次的值!// JavaScript:同样的引用捕获
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => i);
}
console.log(funcs.map(f => f())); // [3, 3, 3]// Go:也是引用捕获
funcs := make([]func() int, 3)
for i := 0; i < 3; i++ {
funcs[i] = func() int { return i }
}
// 全是 3C++ 是例外——它让你自己选:
// C++:显式控制
std::vector<std::function<int()>> funcs;
for (int i = 0; i < 3; i++) {
funcs.push_back([i]() { return i; }); // [i] = 值捕获!每个闭包有自己的 i
}
// [0, 1, 2] —— 每个闭包持有独立的副本Rust 也让你选,但方式不同:
let mut funcs: Vec<Box<dyn Fn() -> i32>> = vec![];
for i in 0..3 {
funcs.push(Box::new(move || i)); // move = 所有权转移到闭包里
}为什么引用捕获是默认的
因为大多数时候你希望在闭包里看到外部变量的最新值:
count = 0
def increment():
global count
count += 1 # 你希望修改外部变量
return count但问题是:循环中创建的闭包,经常不是这种情况。 这就是 "循环 + 闭包" 陷阱在几乎所有语言里都存在的原因。
各语言的惯用避坑法
# Python:用默认参数在定义时绑定值
funcs = [lambda i=i: i for i in range(3)] # [0, 1, 2]// JavaScript:用 let 代替 var(let 有块级作用域,每次迭代创建新绑定)
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(() => i);
}
console.log(funcs.map(f => f())); // [0, 1, 2]// Go:在循环体内复制变量
funcs := make([]func() int, 3)
for i := 0; i < 3; i++ {
i := i // 创建循环局部副本
funcs[i] = func() int { return i }
}// Kotlin:forEach 的 it 每次都是新的
val funcs = (0..2).map { i -> { i } } // [0, 1, 2]5.3 深度对照二:this / self 的三条路
一个方法里要引用 "我自己",8 门语言的分歧出人意料地大。
路线一:隐式 this(Java, C++, JavaScript, Kotlin)
// Java
class OrderProcessor {
private String prefix;
public void process(Order order) {
this.prefix = ">> "; // this 可选(除了参数同名时)
log(prefix + order.orderId()); // 直接访问成员
}
}// C++
class OrderProcessor {
std::string prefix;
void process(const Order& order) {
this->prefix = ">> "; // this-> 可选(除了模板中的依赖名)
log(prefix + order.order_id);
}
};// Kotlin
class OrderProcessor {
var prefix = ""
fun process(order: Order) {
this.prefix = ">> " // this 可选
log(prefix + order.orderId)
}
}路线二:显式 self(Python, Rust, Swift)
# Python:self 是显式参数(但不传)
class OrderProcessor:
prefix: str = ""
def process(self, order: Order) -> None:
self.prefix = ">> " # 必须写 self.
log(self.prefix + order.order_id)// Rust:self 有多种形式
impl OrderProcessor {
fn process(&self, order: &Order) { // &self = 不可变借用
log(&format!("{}{}", self.prefix, order.order_id));
}
fn reset(&mut self) { // &mut self = 可变借用
self.prefix = String::new();
}
fn consume(self) -> String { // self = 获取所有权
self.prefix
}
}// Swift:self 通常隐式,闭包内显式
class OrderProcessor {
var prefix = ""
func process(_ order: Order) {
self.prefix = ">> " // 通常不需要 self
async { [weak self] in
self?.prefix = "done" // 闭包内必须显式 self
}
}
}路线三:接收者作为第一个参数(Go)
// Go:方法就是带接收者参数的函数
type OrderProcessor struct {
prefix string
}
func (op *OrderProcessor) Process(order Order) {
op.prefix = ">> " // 通过接收者名访问(不是 this/self)
log.Print(op.prefix + order.OrderID)
}Go 没有 this 或 self——接收者是一个普通的参数名,你可以叫它任何名字(惯用是类型名的缩写,如 op、o)。
this/self 的真正分歧在哪
不只是语法。是语言对 "方法到底是什么" 的根本分歧:
- 隐式 this 路线:方法是一个和对象绑定的特殊函数。
this.prefix告诉你 "这是这个对象的 prefix"。 - 显式 self 路线:方法就是一个普通函数,self 是它的第一个参数。Python 的
def process(self, order)让这件事异常直白——你可以直接把OrderProcessor.process当普通函数调用:OrderProcessor.process(processor_instance, order)。 - Go 路线:方法本质上就是带接收者的函数。你可以把
OrderProcessor.Process当成func(op *OrderProcessor, order Order)来用——这叫 method value。
5.4 深度对照三:高阶函数——谁更擅长操作行为
一个经典的订单处理流水线
# Python
result = (
orders
| filter(lambda o: o.paid)
| map(lambda o: o.amount)
| sum
)注意 Python 用 | 而不是 .——这是 Python 社区对管道操作符的妥协方案。
// Kotlin
val result = orders
.filter { it.paid }
.map { it.amount }
.sum()// Rust
let result: f64 = orders.iter()
.filter(|o| o.paid)
.map(|o| o.amount)
.sum();// Swift
let result = orders
.filter { $0.paid }
.map { $0.amount }
.reduce(0, +)// JavaScript
const result = orders
.filter(o => o.paid)
.map(o => o.amount)
.reduce((sum, a) => sum + a, 0);// Java
double result = orders.stream()
.filter(Order::paid)
.mapToDouble(Order::amount)
.sum();// C++20 ranges
auto result = orders
| std::views::filter([](const Order& o) { return o.paid; })
| std::views::transform([](const Order& o) { return o.amount; });
// 求和还需要额外的 reduce// Go
var result float64
for _, o := range orders {
if o.Paid {
result += o.Amount
}
}三种风格
链式调用派(Kotlin、Swift、Rust、JavaScript):用 . 串联,代码从左读到右。最接近自然语言。
管道派(Python、C++20):用 | 串联,函数独立可复用。但需要语言支持管道操作符或 ranges 库。
显式循环派(Go):没有魔法,任何人都看得懂。代价是逻辑和迭代控制混在一起。
哪种更好?和大多数工程问题一样——取决于谁来维护。 Kotlin 的链式调用对习惯函数式编程的人如呼吸般自然,但对只写过 Java 6 的人可能需要一个适应期。Go 的 for 循环任何人都能看懂,但在 20 个连续的过滤-转换-聚合操作之后,代码开始变得臃肿。
5.5 边界探索:迁移时最容易踩的函数坑
陷阱一:JavaScript → Kotlin/Rust:return 在 lambda 里的含义
// JavaScript:return 从 lambda 返回
const result = orders.map(o => {
if (o.paid) return o.amount; // 从 lambda 返回
return 0;
});// Kotlin:return 在 lambda 里的行为取决于上下文
orders.map { o ->
if (o.paid) return o.amount // 危险!这会从外层函数返回!(non-local return)
0
}
// 正确写法:用标签返回或直接用表达式
orders.map { o ->
if (o.paid) return@map o.amount // 从 lambda 返回
0
}Kotlin 的 non-local return 是一个刻意设计的特性(让 lambda 在某些上下文中可以像语言内建控制流一样工作),但也非常容易踩。
陷阱二:Python → Go:没有默认参数就没有惯用模式
# Python:默认参数让你可以做很多事
def process(order, timeout=30, retries=3):
pass// Go:没有默认参数。你的选择:
// 1. 多写几个函数
func Process(order Order) { ProcessWithConfig(order, 30, 3) }
func ProcessWithConfig(order Order, timeout, retries int) {}
// 2. 用 Functional Options 模式
type Option func(*config)
func WithTimeout(t int) Option { return func(c *config) { c.timeout = t } }
func Process(order Order, opts ...Option) {}陷阱三:Rust → Python:习惯了闭包的三种 trait
// Rust 程序员脑中有一个 mental model:Fn vs FnMut vs FnOnce
let apply = |o: &Order| o.amount * rate; // Fn切到 Python 后,所有闭包都是一样的——没有借用检查,没有 move 语义,没有 trait 区分。你会突然觉得 "自由了",但也会失去 Rust 闭包的 predictablity。
陷阱四:Java → Swift/Kotlin:final / effectively final 的差异
// Java:lambda 只能捕获 effectively final 的变量
int x = 10;
Runnable r = () -> System.out.println(x);
// x = 20; // 编译错误:lambda 里用了 x,x 必须是 effectively final// Kotlin:没有这个限制
var x = 10
val r: () -> Unit = { println(x) }
x = 20
r() // 打印 20——捕获的是变量引用5.6 洞察:函数带走了什么,比你想象的多
如果你只用一门语言,你不会发现:
一个函数带走的不仅是它捕获的变量。它带走的是整个对象树的引用、整个生命周期的约束、整个并发模型的前提假设。
同一个闭包,在 Java 里它只捕获 effectively final 变量(值语义的承诺),在 Python 里它捕获变量引用(允许你看到最新值,但也容易制造循环引用),在 Rust 里它按捕获方式被分类为 Fn/FnMut/FnOnce(每种都有自己的借用规则),在 Swift 里它可能正在制造一个强引用环(除非你手动写 [weak self])。
你看不到这些差异——直到你在 Instruments 里看到一个永远不会释放的 ViewController,或者在 valgrind 里看到本该释放的内存还挂着,或者在生产日志里看到本该为 0.1 的折扣率变成了 0.3。
闭包不是语法。它是语言对 "函数和环境的关系" 的治理制度。 弱治理的语言(Python、JavaScript、Go)给你速度和自由,但要求你更警觉。强治理的语言(Rust、Swift、C++)让你写更多声明,但替你守住更多边界。
本章小结
闭包不是魔法——它是捕获语义在不同语言里的不同实现。 引用捕获(Python、JS、Go)、显式捕获控制(C++)、trait 分级捕获(Rust)、弱引用捕获列表(Swift)——你需要知道你的语言默认了什么。
this/self的三条路线:隐式(Java/C++/JS/Kotlin)、显式(Python/Rust)、无特殊关键词(Go)。这不仅仅是语法——它决定了方法到底是什么。高阶函数的三种风格:链式调用、管道、显式循环。选择不是关于 "现代性",而是关于团队背景和维护预期。
Lambda return 的语义在语言之间不一致。 Kotlin 的 non-local return、Rust 的闭包 trait 体系、Java 的 effectively final——这些都是跨语言迁移的高危地带。
如果你只用一门语言,你不会学到这个:
同一个 lambda 表达式,就像同一份租赁合同——在有的国家它租的是一个值,在有的国家它租的是整个房子,在有的国家它只是拿到了房子的钥匙,而在 Rust,你得先证明你有权在租赁期内进入这间房子。