Python 的 yield 是一个高级语言特性,用于实现生成器,它背后没有直接的 C 语言等价物。 C 语言本身没有生成器或协程的原生语法,我们可以用 C 语言的各种技术来模拟 yield 的行为。

(图片来源网络,侵删)
下面我将从三个层面来详细解释:
- Python
yield是什么(回顾基础) - 为什么 C 语言没有
yield(语言设计的差异) - 如何在 C 语言中模拟
yield的行为)
Python yield 是什么?
yield 关键字将一个普通函数变成一个生成器函数。
- 普通函数:被调用时,从函数头开始执行,直到
return语句或函数末尾,然后一次性返回所有结果,并销毁其局部状态。 - 生成器函数:被调用时,它返回一个生成器对象,但函数体内的代码不会立即执行,当代码通过这个生成器对象的
__next__()方法(例如在for循环中)被驱动时,函数才会从上次暂停的地方恢复执行,直到再次遇到yield语句。yield会“吐出”一个值,并暂停函数的状态(包括所有局部变量),等待下一次被唤醒。
一个经典的例子:斐波那契数列
def fibonacci(n):
"""一个生成器函数,用于生成斐波那契数列"""
a, b = 0, 1
count = 0
while count < n:
# yield 暂停函数,并返回 a 的值
yield a
# 当函数被恢复时,从这里继续执行
a, b = b, a + b
count += 1
# 使用生成器
fib = fibonacci(10) # 调用函数,返回一个生成器对象,但不执行函数体
print(type(fib)) # <class 'generator'>
# for 循环会隐式地调用 next(fib)
for number in fib:
print(number)
# 输出:
# 0
# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34
核心特点:

(图片来源网络,侵删)
- 状态保存:函数在
yield后暂停时,其所有局部变量(a,b,count)都会被保留。 - 惰性求值:只有在需要下一个值时,代码才会执行,这对于处理大数据流或无限序列非常高效。
- 协程基础:
yield不仅可以yield值,还可以接收值(value = yield x),这使得 Python 的生成器可以成为强大的协程。
为什么 C 语言没有 yield?
C 语言的设计哲学是“接近硬件”和“高效”,它的核心是基于栈的函数调用模型。
- 栈帧的销毁:当一个 C 函数被调用时,会在栈上分配一个“栈帧”,用于存储参数、返回地址和局部变量,当函数执行到
return语句时,对应的栈帧就会被销毁,函数的控制权完全返回给调用者,函数内部的状态随之丢失。 - 没有“暂停”机制:C 语言没有内置的机制可以让一个函数在执行到一半时“暂停”自己,并将控制权交还给调用者,同时还保留其栈帧和所有局部变量,以便将来可以从暂停点恢复。
- 控制流简单:C 语言的控制流结构(
if,for,while,switch,function calls)都是结构化的,但它们无法实现生成器这种非本地的、可恢复的“跳转”。
C 语言没有 yield 这样的关键字,因为它与 C 的基本执行模型是相悖的。
如何在 C 语言中模拟 yield 的行为?
既然没有原生支持,我们就必须自己构建一个框架来实现它,这通常涉及以下几个技术点:
- 函数指针:用于保存下一次要执行的代码位置。
- 状态机:将生成器函数的逻辑分解成多个状态,用一个变量来记录当前执行到哪个状态。
- 结构体:用于封装生成器的所有状态,包括局部变量和函数指针。
setjmp/longjmp:这是 C 标准库提供的一对强大(但危险)的函数,可以实现非局部的跳转,是模拟yield的最底层、最强大的工具。
下面我们用两种方法来实现斐波那契数列的生成器。
使用状态机(最常见、最直观)
这种方法将生成器的逻辑分解为多个步骤,并用 switch 语句来管理状态。
代码示例:
#include <stdio.h>
// 1. 定义生成器的状态
typedef enum {
FIB_START,
FIB_CALC,
FIB_DONE
} FibState;
// 2. 定义生成器的结构体,封装所有状态
typedef struct {
int a, b;
int count;
int n;
FibState state;
} FibonacciGenerator;
// 3. 初始化生成器
void fib_init(FibonacciGenerator* gen, int n) {
gen->a = 0;
gen->b = 1;
gen->count = 0;
gen->n = n;
gen->state = FIB_START;
}
// 4. 模拟 yield 的函数
// 这个函数可以被反复调用,每次返回一个值或结束信号
int fib_next(FibonacciGenerator* gen, int* value) {
switch (gen->state) {
case FIB_START:
// 第一次进入,准备返回第一个值
gen->state = FIB_CALC;
*value = gen->a;
return 1; // 表示有值返回
case FIB_CALC:
// 恢复执行,进行计算
gen->a = gen->b;
gen->b = gen->a + gen->b; // 注意:这里为了和Python一致,逻辑上应该是 a, b = b, a+b
// 修正逻辑:
int temp = gen->a;
gen->a = gen->b;
gen->b = temp + gen->b;
gen->count++;
if (gen->count < gen->n) {
*value = gen->a;
return 1; // 继续返回值
} else {
gen->state = FIB_DONE;
return 0; // 结束
}
case FIB_DONE:
default:
return 0; // 已结束
}
}
int main() {
FibonacciGenerator fib;
fib_init(&fib, 10);
int value;
// 模拟 for number in fib:
while (fib_next(&fib, &value)) {
printf("%d\n", value);
}
return 0;
}
优点:
- 逻辑清晰,易于理解和调试。
- 不依赖非标准或危险的库函数。
- 性能很好。
缺点:
- 需要将原始函数的逻辑重构为状态机,当逻辑复杂时,代码会变得冗长和繁琐。
使用 setjmp/longjmp(更接近 Python 的行为)
setjmp 和 longjmp 提供了一种“保存上下文”和“恢复上下文”的能力,这与生成器的暂停和恢复非常相似。
setjmp(jmp_buf env): 保存当前的调用栈上下文到env中,如果第一次调用,它返回 0。longjmp(jmp_buf env, int val): 从env中恢复之前保存的上下文,程序会跳转到setjmp调用的位置,并作为返回值返回val(不能为 0)。
代码示例:
#include <stdio.h>
#include <setjmp.h>
// jmp_buf 用于保存上下文
static jmp_buf jump_env;
// 定义生成器的结构体,局部变量保存在这里
typedef struct {
int a, b;
int count;
int n;
} FibonacciGenerator;
// 模拟 yield 的宏
#define YIELD(value) do { \
int yielded_value = (value); \
longjmp(jump_env, yielded_value + 1); /* 返回值+1,因为0是setjmp的初始返回 */ \
} while(0)
// 模拟生成器函数
// 这个函数在第一次调用时开始,每次 longjmp 后都会从这里恢复
void fibonacci_generator(FibonacciGenerator* gen) {
gen->a = 0;
gen->b = 1;
gen->count = 0;
while (gen->count < gen->n) {
YIELD(gen->a); // 暂停并返回 a
// 当 longjmp 恢复时,代码会从这里继续执行
int temp = gen->a;
gen->a = gen->b;
gen->b = temp + gen->b;
gen->count++;
}
}
int main() {
FibonacciGenerator fib;
// 第一次调用 setjmp,保存 main 的上下文
// 它会返回 0
int ret = setjmp(jump_env);
if (ret == 0) {
// 第一次进入,启动生成器
fibonacci_generator(&fib);
} else {
// 被 longjmp 跳转回来,ret 是 longjmp 传入的值
// 减 1 得到 yield 的原始值
int yielded_value = ret - 1;
printf("%d\n", yielded_value);
// 检查生成器是否应该继续
if (fib.count < fib.n) {
// 再次调用 setjmp,等待下一次 longjmp
// 这会覆盖 jump_env 中的旧上下文
setjmp(jump_env);
// 从这里恢复后,再次进入生成器函数
fibonacci_generator(&fib);
}
}
return 0;
}
注意:上面的 main 函数写法只是为了演示 setjmp/longjmp 的跳转机制,一个更实用的封装会像方法一那样提供一个 fib_next 函数来驱动生成器,但内部使用 longjmp 来实现状态的恢复。
优点:
- 可以让原始的生成器逻辑(
while循环)保持不变,不像状态机那样需要重构。 - 更接近 Python
yield的“魔法感”。
缺点:
- 非常危险:
longjmp会绕过正常的栈展开机制,这意味着在longjmp和setjmp之间的栈帧上的局部变量的析构函数(C++)不会被执行,可能导致资源泄漏(如未关闭的文件、未释放的内存)。 - 代码可读性差,调试困难,容易出错。
- 是“非结构化”的控制流,滥用会使程序难以维护。
总结对比
| 特性 | Python yield |
C 语言 (状态机) | C 语言 (setjmp/longjmp) |
|---|---|---|---|
| 语言支持 | 原生语法,一等公民 | 手动模拟,无语法糖 | 手动模拟,使用标准库 |
| 实现原理 | 协程和栈帧的自动保存/恢复 | switch 语句 + 结构体 |
setjmp/longjmp 上下文切换 |
| 代码可读性 | 非常高,逻辑清晰 | 中等,需要重构为状态机 | 差,控制流混乱 |
| 安全性 | 安全,由解释器管理 | 安全,完全可控 | 危险,可能导致资源泄漏 |
| 适用场景 | 生成器、协程、异步编程 | 嵌入式系统、游戏逻辑、协议解析 | 底层系统编程、调试器等特殊场景 |
对于 C 语言开发者来说,使用状态机是模拟 yield 行为最推荐、最安全、最实用的方法,虽然它需要你将逻辑进行重构,但它带来的清晰度和可控性是值得的。setjmp/longjmp 则是一种“大杀器”,应该在非常特殊且清楚其风险的情况下才使用。
Python 的 yield 是一个语言级别的优雅抽象,而 C 语言则要求你直面其底层的复杂性来实现类似的功能,这正是两种语言在设计哲学上的一个典型体现。
