闲话 + .Net yeild语句

本文通过几个实例详细解析了C#中yield语句的工作原理及其在迭代器中的运用方式,强调了yield return和yield break的作用,并展示了如何避免资源浪费。

好久不写了,总想接着之前的写,但是随着mvc慢慢熟悉,不知道写什么了,又因为比较忙,也懒得写了。

于是写点小玩意儿吧。其实.net从开始写,就是写写asp.net。那时候,对前端后端怎么交互的,还一知半解的。用了asp.net,更是似乎不需要深入理解其原理,也能像写winform一样写web应用。拖拖控件,写写事件,就完了。后来慢慢研究.net,发现.net框架确实是不折不扣的重量级库和运行时,想要实现的功能,基本都能找到官方的封装,虽然不一定好用。再后来,接触Linq,Lambada,Expression,Attribute,才发现.net有这么多新颖高端的玩意儿,跟之前写c++完全不同。

今儿个说说yield语句吧。yield语句分为yield return (value)和yeild break,前者用于迭代返回迭代器接口数据,后者退出迭代过程。

刚接触的时候,各种不理解。后来理解了一点,觉得用了这货的方法应该是异步执行的吧。再后来,做了个小实验,才明白这货是怎么玩的。

考虑下面的简单代码:

class Program
    {
        public static IEnumerable<int> GetIEnum()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("a:" + i);
                yield return i;
            }
        }
        static void Main(string[] args)
        {
            var data = GetIEnum();
            foreach (var i in data)
            {
                Console.WriteLine("b:" + i);
            }
        }
    }
运行的结果会是什么呢?

没错,不是随机顺序,而是很规律的a、b交替出现:

a:0

b:0

a:1

b:1

……

从上面看出迭代方法的执行不是异步的,而是每执行一次迭代方法中的for,就再执行一次main里的foreach

如果稍微加点代码我们可以看出他也不是先执行到第一个yield return停止执行,然后返回执行调用方法的下条语句的:

class Program
    {
        public static IEnumerable<int> GetIEnum()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("a:" + i);
                yield return i;
            }
        }
        static void Main(string[] args)
        {
            var data = GetIEnum();
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("c:" + i);
            }
            foreach (var i in data)
            {
                Console.WriteLine("b:" + i);
            }
        }
    }
这回是什么呢
结果是这样:

c:0

c:1

c:2

……

a:0

b:0

a:1

b:1

……

说明在调用

var data = GetIEnum();
时候,GetIEnum方法压根没有执行一条语句,而是直接返回,等到遇到foreach遍历结果的时候,才会开始迭代。

再次修改代码我们能看到迭代的运行机制:

class Program
    {
        public static IEnumerable<int> GetIEnum()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("a:" + i);
                yield return i;
                Console.WriteLine("d:" + i);
            }
        }
        static void Main(string[] args)
        {
            var data = GetIEnum(); 
                foreach (var i in data)
                {
                         Console.WriteLine("b:" + i); 
                }
        }
    }

在yield return后面再次输出,执行结果如下:

a:0

b:0

d:0

a:1

b:1

d:1

……

结果说明yield return是如何工作的:在执行到foreach语句迭代获取迭代方法返回结果时,迭代方法才开始运行,这时,会从第一条语句开始执行,直到遇到yield return。此时,方法的执行状态将被保存,跳转回foreach继续执行,将return的值作为foreach取出的元素,执行完foreach体后,又会跳回之前yield return的语句的下一条,继续执行,直到下一条yield return,继续foreach体-迭代方法的循环,直到运行到迭代过程结束,或者遇到yield break跳出迭代过程为止。

由此看,似乎在运行到foreach语句的时候,角色发生了变化,迭代方法似乎成了调用者,foreach块倒成了被调用者,yield return语句相当于做了一次foreach块的调用,何时调用结束取决于迭代方法何时结束。

为了验证上面的推断是否正确,我们再修改下代码:

class Program
    {
        public static IEnumerable<int> GetIEnum()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("a:" + i);
                yield return i;
                yield return i;
            }
        }
        static void Main(string[] args)
        {
            var data = GetIEnum();
            foreach (var i in data)
            {
                Console.WriteLine("b:" + i);
            }
        }
    }
结果不出意外:

a:0

b:0

b:0

a:1

b:1

b:1

……

foreach块看起来就像是一个方法,在迭代过程每次yield return时都会被调用一次。

那么,使用迭代过程延后获取数据的时候,如果要多次使用foreach遍历,似乎有些要担心了。迭代方法会不会不缓存数据,在每次调用foreach时,迭代方法都要运行一次?

我们试一下:

class Program
    {
        public static IEnumerable<int> GetIEnum()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("a:" + i);
                yield return i;
            }
        }
        static void Main(string[] args)
        {
            var data = GetIEnum();
            foreach (var i in data)
            {
                Console.WriteLine("b:" + i);
            }
            foreach (var i in data)
            {
                Console.WriteLine("c:" + i);
            }
        }
    }

结果很悲剧的印证了:

a:0

b:0

a:1

b:1

……

a:0

c:0

a:1

c:1

……

可以看到,用foreach遍历多少次迭代器方法的结果,就会导致迭代方法执行多少次,如果迭代方法获取数据本身比较耗时或者消耗资源,foreach的次数不止一次的话,这个方法会造成资源浪费。所以这种时候,还是老实把结果一次取出,存到Array或者List里吧。

就讨论这么多吧,以后继续……

<think> 嗯,用户想了解Python中yield函数的相关信息。根据用户要求和提供的引用材料,我需要整理一个全面但易懂的介绍。 用户明确要求"yield函数介绍",结合引用内容来看,重点应该放在基础概念和核心特性上。引用[1][2][3]都强调了yield与生成器的关系,引用[3]提到的惰性求值特性特别关键,引用[4][5]则展示了具体应用场景和工作原理。 准备从这几个维度展开:首先用比喻解释生成器的本质——就像按需生产的流水线。然后重点说明yield的三个核心特征:1) 暂停执行保留状态(对比return的彻底退出)2) 通过next()或循环触发执行 3) 生成器对象的迭代协议支持。最后列举文件遍历/大数据处理等典型场景。 需要特别注意避免术语堆砌。比如解释"惰性求值"时,会直接关联到读取大文件的例子:"想象打开10GB日志文件但只处理首行,yield可以做到不加载全部内容"。对于协程通信(send()方法)这类高级用法,考虑点到为止,毕竟用户当前可能只需要基础认知。 相关问题的设计要体现知识梯度:首问巩固基础概念(生成器标识),次问拓展应用(实际场景),末问挑战理解(与普通函数对比)。这样既覆盖即时需求,也为后续学习留了接口。 </think> 好的,Python 中的 `yield` 关键字是理解**生成器**的核心。它不是一个独立的“函数”,而是用于定义生成器函数的一个**关键字**。下面是根据您提供的引用信息,对 `yield` 和生成器的详细介绍: ### 1. yield 与生成器 (Generator) * **核心概念:** 当一个函数内部使用了 `yield` 关键字后,它就变成了一个**生成器函数**(Generator Function)[^2][^3]。 * **普通函数 vs 生成器函数:** * **普通函数:** 使用 `return` 返回结果。调用时,函数从头到尾执行一次,计算并返回一个值(或None),执行完毕后其内部状态(局部变量等)就被销毁了。 * **生成器函数:** 使用 `yield` 产生值。调用生成器函数时,**它不会立即执行函数体内的代码**,而是**返回一个生成器对象(Generator Object)**[^3]。这个生成器对象遵守**迭代器协议**(Iterator Protocol)。 ### 2. 基本原理与工作方式 * **惰性求值 (Lazy Evaluation):** 这是生成器最核心的优点[^3]。生成器函数不会一次性计算所有结果。 * **执行流程:** 1. **调用生成器函数:** 当你调用 `gen_func()` 时,它返回一个生成器对象 `gen_obj`,但函数体代码**尚未执行**。 2. **首次迭代 (如调用 `next(gen_obj)` 或 `for` 循环开始):** 代码开始执行,直到遇到第一个 `yield` 语句。 3. **`yield` 暂停:** 遇到 `yield expr` 时: * 计算 `expr` 的值。 * 将这个值作为 `next()` 方法的**当前返回值**传递给调用者。 * **暂停**生成器函数的执行,并**保存当前所有的局部变量状态**(包括指令指针位置)。 4. **下一次迭代:** 当再次调用 `next(gen_obj)` 或在 `for` 循环的下一次迭代中: * 生成器函数**从上次暂停的 `yield` 语句之后**恢复执行(状态完全保留)。 * 继续执行代码,直到遇到下一个 `yield` 语句(然后暂停并返回值)或函数结束。 5. **函数结束:** 当生成器函数执行到 `return` 语句(可以没有显式 `return`,隐含 `return None`)或函数体结束时,会抛出一个特殊的 `StopIteration` 异常,标志迭代结束[^5]。 ```python def simple_generator(): print("Starting...") yield 1 # 暂停点 1 print("Resuming...") yield 2 # 暂停点 2 print("Ending...") # 函数结束,触发 StopIteration gen = simple_generator() # 调用函数,返回生成器对象,打印 Starting... print(next(gen)) # 恢复执行直到第一个 yield,输出 1 print(next(gen)) # 从 yield 1 后恢复,打印 "Resuming...",输出 2 print(next(gen)) # 从 yield 2 后恢复,打印 "Ending...", 抛出 StopIteration(需要 try/catch 处理) ``` ### 3. 关键特性与优势 * **状态保持:** 每次 `yield` 暂停时,生成器函数内部的局部变量和执行状态都会被完美保存,下次恢复时完全接手,这使得它可以轻松实现复杂的状态机逻辑[^1]。 * **内存高效:** 由于惰性求值,生成器**一次只产生(和处理)一个值**。这对于处理**超大文件**(如日志文件)或**无限序列**(如斐波那契数列)特别有用,避免了将所有数据一次性加载到内存中导致的崩溃[^3][^4]。 * **流式处理 (Stream Processing):** 非常适合管道式操作或持续生成数据流的场景[^3]。 * **可迭代:** 生成器对象是天然的迭代器,可以直接用在 `for` 循环中。 ### 4. 高级用法 (基于引用 [^1][^5]) * **`.send(value)` 方法:** 除了 `next()` 可以恢复执行,`send(value)` 方法也能恢复生成器执行,并且**可以将一个值 `value` 发送到生成器内部**。这个发送的值会成为**暂停处的 `yield` 表达式的结果**。这实现了**双向通信**,使生成器可以变成**协程 (Coroutine)** 的基础。 ```python def generator_with_send(): print("Started, waiting for input...") received = yield "First yield" # 暂停,等待 send 输入 print(f"Received: {received}") yield "Second yield" gen = generator_with_send() first_val = next(gen) # 启动生成器,获取第一个 yield 的值 ("First yield") print(first_val) # 输出:First yield second_val = gen.send("Hello!") # 发送 "Hello!" 进去,received 被赋值 "Hello!",执行到下一个 yield print(second_val) # 输出:Second yield ``` * **`yield from` 表达式:** 用于**委托**给另一个生成器(或任何可迭代对象)。它可以简化生成器嵌套的写法,并将值传递和异常处理的责任委托给子生成器[^1][^3]。 ```python def gen1(): yield 'A' yield 'B' def gen2(): yield from gen1() # 委托给 gen1 yield 'C' for item in gen2(): print(item) # 输出 A, B, C ``` ### 5. 主要应用场景 * **处理大型数据集/文件:** 逐行读取文件而不加载整个文件到内存[^4]。 * **生成无限序列:** 如斐波那契数列、素数序列。 * **实现管道 (Pipeline):** 将多个生成器串联起来,形成一个数据处理管道。 * **状态机建模:** 利用暂停恢复和状态保持的特性。 * **协程 (基于 `yield` / `send`):** 构建更高级的并发模式(虽然 `asyncio` 是现代更主流的协程库,但其概念基础与生成器相关)。 ### 6. 总结 `yield` 关键字的出现标志着一个函数是生成器函数。生成器函数返回一个生成器对象。生成器的核心在于**惰性求值**和在 `yield` 处**暂停执行并保存状态**的能力。这使得它们极其高效地处理大数据流、无限序列,并简化迭代逻辑。高级用法如 `.send(value)` 和 `yield from` 进一步增强了其表达能力和在协程中的应用潜力[^1][^2][^3][^5]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值