Skip to content

第 5 章:捕获 — 一个函数不只是代码块

一个让你睡不着觉的闭包问题

先看这段代码。它看起来无害:

python
def make_discount_calculator(discount_rate):
    def apply(order):
        return order.amount * discount_rate
    return apply

然后你写了:

python
calculators = []
for rate in [0.1, 0.2, 0.3]:
    calculators.append(make_discount_calculator(rate))

这段代码没问题。但如果是这样呢:

python
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

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.0

JavaScript / TypeScript

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

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++

cpp
// 函数
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

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

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

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

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)
}

语法速查:函数与闭包

能力PythonTypeScriptJavaC++RustGoSwiftKotlin
lambda/匿名函数lambda x: exprx => exprx -> expr[](auto x){...}|x| exprfunc(x T) T {...}{ $0 ... }{ it ... }
闭包捕获引用捕获引用捕获effectively final 值显式控制 [=] / [&]按 trait 区分 Fn/FnMut/FnOnce引用捕获默认强引用,可用 [weak self]引用捕获
函数类型写法Callable[[In], Out](x: In) => OutFunction<In, Out>std::function<Out(In)>fn(In) -> Outfunc(In) Out(In) -> Out(In) -> Out
高阶函数map(f)map(f)stream().map(f)std::transformiter().map(f)for 循环map(f)map(f)
尾随闭包语法
带接收者的 lambda

5.2 深度对照一:闭包捕获了什么——不是所有语言都一样

这是跨语言迁移中最容易被忽视、却最容易出问题的差异。

引用捕获 vs 值捕获

大多数语言默认引用捕获:闭包里用到的外部变量,捕获的是变量的引用(或指针),而不是创建时的值。

python
# Python:默认引用捕获
funcs = []
for i in range(3):
    funcs.append(lambda: i)  # 捕获变量 i 的引用
print([f() for f in funcs])  # [2, 2, 2] —— 全是最后一次的值!
javascript
// JavaScript:同样的引用捕获
const funcs = [];
for (var i = 0; i < 3; i++) {
    funcs.push(() => i);
}
console.log(funcs.map(f => f()));  // [3, 3, 3]
go
// Go:也是引用捕获
funcs := make([]func() int, 3)
for i := 0; i < 3; i++ {
    funcs[i] = func() int { return i }
}
// 全是 3

C++ 是例外——它让你自己选:

cpp
// 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 也让你选,但方式不同:

rust
let mut funcs: Vec<Box<dyn Fn() -> i32>> = vec![];
for i in 0..3 {
    funcs.push(Box::new(move || i));  // move = 所有权转移到闭包里
}

为什么引用捕获是默认的

因为大多数时候你希望在闭包里看到外部变量的最新值:

python
count = 0
def increment():
    global count
    count += 1  # 你希望修改外部变量
    return count

但问题是:循环中创建的闭包,经常不是这种情况。 这就是 "循环 + 闭包" 陷阱在几乎所有语言里都存在的原因。

各语言的惯用避坑法

python
# Python:用默认参数在定义时绑定值
funcs = [lambda i=i: i for i in range(3)]  # [0, 1, 2]
javascript
// 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
// Go:在循环体内复制变量
funcs := make([]func() int, 3)
for i := 0; i < 3; i++ {
    i := i  // 创建循环局部副本
    funcs[i] = func() int { return i }
}
kotlin
// Kotlin:forEach 的 it 每次都是新的
val funcs = (0..2).map { i -> { i } }  // [0, 1, 2]

5.3 深度对照二:this / self 的三条路

一个方法里要引用 "我自己",8 门语言的分歧出人意料地大。

路线一:隐式 this(Java, C++, JavaScript, Kotlin)

java
// Java
class OrderProcessor {
    private String prefix;
    public void process(Order order) {
        this.prefix = ">> ";  // this 可选(除了参数同名时)
        log(prefix + order.orderId());  // 直接访问成员
    }
}
cpp
// C++
class OrderProcessor {
    std::string prefix;
    void process(const Order& order) {
        this->prefix = ">> ";  // this-> 可选(除了模板中的依赖名)
        log(prefix + order.order_id);
    }
};
kotlin
// Kotlin
class OrderProcessor {
    var prefix = ""
    fun process(order: Order) {
        this.prefix = ">> "  // this 可选
        log(prefix + order.orderId)
    }
}

路线二:显式 self(Python, Rust, Swift)

python
# Python:self 是显式参数(但不传)
class OrderProcessor:
    prefix: str = ""

    def process(self, order: Order) -> None:
        self.prefix = ">> "  # 必须写 self.
        log(self.prefix + order.order_id)
rust
// 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
// Swift:self 通常隐式,闭包内显式
class OrderProcessor {
    var prefix = ""
    func process(_ order: Order) {
        self.prefix = ">> "  // 通常不需要 self
        async { [weak self] in
            self?.prefix = "done"  // 闭包内必须显式 self
        }
    }
}

路线三:接收者作为第一个参数(Go)

go
// Go:方法就是带接收者参数的函数
type OrderProcessor struct {
    prefix string
}

func (op *OrderProcessor) Process(order Order) {
    op.prefix = ">> "  // 通过接收者名访问(不是 this/self)
    log.Print(op.prefix + order.OrderID)
}

Go 没有 thisself——接收者是一个普通的参数名,你可以叫它任何名字(惯用是类型名的缩写,如 opo)。

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
# Python
result = (
    orders
    | filter(lambda o: o.paid)
    | map(lambda o: o.amount)
    | sum
)

注意 Python 用 | 而不是 .——这是 Python 社区对管道操作符的妥协方案。

kotlin
// Kotlin
val result = orders
    .filter { it.paid }
    .map { it.amount }
    .sum()
rust
// Rust
let result: f64 = orders.iter()
    .filter(|o| o.paid)
    .map(|o| o.amount)
    .sum();
swift
// Swift
let result = orders
    .filter { $0.paid }
    .map { $0.amount }
    .reduce(0, +)
javascript
// JavaScript
const result = orders
    .filter(o => o.paid)
    .map(o => o.amount)
    .reduce((sum, a) => sum + a, 0);
java
// Java
double result = orders.stream()
    .filter(Order::paid)
    .mapToDouble(Order::amount)
    .sum();
cpp
// 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
// 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
// JavaScript:return 从 lambda 返回
const result = orders.map(o => {
    if (o.paid) return o.amount;  // 从 lambda 返回
    return 0;
});
kotlin
// 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
# Python:默认参数让你可以做很多事
def process(order, timeout=30, retries=3):
    pass
go
// 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
// 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
// Java:lambda 只能捕获 effectively final 的变量
int x = 10;
Runnable r = () -> System.out.println(x);
// x = 20;  // 编译错误:lambda 里用了 x,x 必须是 effectively final
kotlin
// 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,你得先证明你有权在租赁期内进入这间房子。