🎬 第十二章 ZLMediaKit搭建直播平台
📄 一句话概括

lambda 的捕获方式决定「什么时候取值」,这一点在 C++ 和 Python 里一样。 按引用捕获配「稍后调用」,是最常见的悬空事故。

📝 本节和前面几节的关系

12.3.1 与 12.3.2 的示例代码里到处是 lambda。 这一节把它单独拎出来,讲清捕获这一件事。

一、C++ 的四种捕获写法

[] 什么都不捕获,[=] 按值捕获全部,[&] 按引用捕获全部, [i] 只按值捕获 i。 四种里前三种是「范围」,第四种是「点名」。

[&] 是事故高发区: lambda 被存起来、稍后才调用时,它引用的局部变量早就出了作用域。 这时读到的是一块已经归还的内存,结果不可预期。 编译不报错,运行时也不一定崩,最难查。

判断标准只有一句话: 这个 lambda 会不会活得比被捕获的变量久? 会,就必须按值捕获。 存进列表、交给线程、注册成回调,都属于「会活得更久」。

二、Python 里一模一样的坑

Python 的闭包默认按「名字所在的格子」取,效果等同按引用。 所以下面两行只差几个字符,结果完全不同:

fs = [lambda: i for i in range(3)]      # 全是 2
fs = [lambda i=i: i for i in range(3)]  # 0、1、2

第一种三个 lambda 共用同一个格子,循环结束时它已经是 2。 第二种用默认参数在「定义那一刻」就把值绑住了,所以各是各的。

这件事可以真看到: a[0].__closure__[0].cell_contents 是 2,b[0].__defaults__ 是 0。 一个指向共用格子,一个存了当场取到的值。 这就是「按引用」与「按值」在 Python 里的实物。

三、两边对照

把这门语言对齐起来看,其实只有一句话: 「按引用」就是「晚点再取」,「按值」就是「现在就取」。

对应的写法是: C++ 的 [&] 对 Python 的 lambda: i; C++ 的 [=] 与 [i] 对 Python 的 lambda i=i: i。

实用建议只有一条: 凡是要把 lambda 存进列表、回调、线程里稍后再调的,一律按值捕获。 只有明确知道被引用的对象活得比自己久时,才用按引用。 这条规矩能挡掉绝大多数「偶尔才复现」的诡异 bug。

四、动手 Demo:lambda——捕获方式决定结果

同一个需求三种写法,真跑出来对比; 再把 Python 的格子与 C++ 的捕获对齐着看。

# ① 名字查找:调用时才取,全都查到同一个 i
a = [lambda: i for i in range(3)]

# ② 默认参数:定义那一刻就把值绑住
b = [lambda i=i: i for i in range(3)]

# ③ 工厂闭包:每个函数捕获自己的 i
def make(i):
    def f():
        return i
    return f
c = [make(i) for i in range(3)]

print([f() for f in a])   # [2, 2, 2]
print([f() for f in b])   # [0, 1, 2]
print([f() for f in c])   # [0, 1, 2]

# 看到底捕获了什么
print(a[0].__closure__[0].cell_contents)   # 2
print(b[0].__defaults__)                   # (0,)

实跑输出:

--------------------------------------------------------------------
lambda——捕获方式决定结果
--------------------------------------------------------------------
**① C++ 的 lambda 有四种捕获写法**

  写法     含义               风险
  ------------------------------------------------------------
  []       什么都不捕获       访问外部变量直接编译失败
  [=]      按值捕获全部       安全,但有拷贝开销
  [&]      按引用捕获全部     最快,但对象先死就是悬空引用
  [i]      按值只捕 i         最常用的写法

  **`[&]` 是事故高发区:**
  **lambda 被存起来、稍后才调用时,它引用的局部变量早就出了作用域。**
  **这时读到的是一块已经归还的内存,结果不可预期。**
  **编译不报错,运行时也不一定崩,最难查。**

**② Python 里有一模一样的坑**

  **Python 的闭包默认也是「按引用」(准确地说是按名字查同一份作用域),**
  **所以下面三种写法的结果完全不同。**

    # ① 名字查找:全都查到同一个 i
    [lambda: i for i in range(3)] → 2、2、2

    # ② 默认参数:生成时就把值绑住了
    [lambda i=i: i for i in range(3)] → 0、1、2

    # ③ 工厂闭包
    [make(i) for i in range(3)] → 0、1、2

  **第一种全是 2,后两种是 0、1、2。**
  **这不是 Python 的 bug,是「什么时候取这个值」的区别:**
  **① 是调用时取,② ③ 是定义时取。**

  **可以真看到它捕获的是什么:**
  **`a[0].__closure__[0].cell_contents` 是 2,**
  **因为三个 lambda 共用同一个格子,而循环结束时它已经是 2。**
  **而 `b[0].__defaults__` 里存的是 0,它在定义那一刻就定死了。**

**③ 对照回 C++ 的结论**

  **上面那三个 lambda 一共共用 1 个格子(一个变量一个格子)。**

  C++ 写法         Python 对应       取值的时刻
  -------------------------------------------------
  [&] 按引用       lambda: i         调用时才取
  [=] 按值         lambda i=i: i     定义时就取
  [i] 指定变量     lambda i=i: i     定义时就取

  **所以这门语言之间是通的:**
  **「按引用」就是「晚点再取」,「按值」就是「现在就取」。**
  **只要想清楚这一点,两边都不会写错。**

  **最后一条实用建议:**
  **凡是要把 lambda 存进列表、回调、线程里稍后再调的,一律用按值捕获。**
  **只有在明确知道被引用的对象活得比自己久时,才用按引用。**

三种写法结果:全是 2、0/1/2、0/1/2。 三个 lambda 共用同一个格子,循环结束时它已经是 2。 默认参数在定义那一刻就定死了值,所以各是各的。 这和 C++ 的 [&] 与 [=] 是同一件事,只是说法不同。

30 秒实验

  1. 把 range(3) 改成 range(5),猜第一种会变成什么——全是 4,共用格子跟着走。
  2. 把 ② 的默认参数删掉,猜和 ① 差在哪——没差别了,都退化成名字查找。
  3. 在线程里用 ① 的写法,猜会出什么——读到的是线程启动后那一刻的值,不是提交时的值。

常见误区

  1. 「lambda 就是匿名函数,和捕获无关」——捕获方式才决定结果。
  2. 「Python 没有按值捕获」——有,用默认参数就是。
  3. 「[&] 更快所以更好」——活得更久时就是悬空引用。
自测三问(点开对答案) 1. C++ 里 `[&]` 的风险是什么? —— **按引用捕获,对象先死就成悬空引用**,不报错且难查。 2. `lambda: i` 与 `lambda i=i: i` 差在哪? —— **前者调用时才取 i,后者定义时就把值绑住。** 3. 什么情况下必须按值捕获? —— **lambda 会比被捕获的变量活得更久时**,比如存进列表或交给线程。

相关笔记