关于 yield 关键字(C#)
〇、前言
yield 关键字的用途是把指令推迟到程序实际需要的时候再执行,这个特性允许我们更细致地控制集合每个元素产生的时机。
对于一些大型集合,加载起来比较耗时,此时最好是先返回一个来让系统持续展示目标内容。类似于在餐馆吃饭,肯定是做好一个菜就上桌了,而不会全部的菜都做好一起上。
另外还有一个好处是,可以提高内存使用效率。当我们有一个方法要返回一个集合时,而作为方法的实现者我们并不清楚方法调用者具体在什么时候要使用该集合数据。如果我们不使用 yield 关键字,则意味着需要把集合数据装载到内存中等待被使用,这可能导致数据在内存中占用较长的时间。
下面就一起来看下怎么用 yield 关键字吧。
一、yield 关键字的使用
1.1 yield return:在迭代中一个一个返回待处理的值
如下示例,循环输出小于 9 的偶数,并记录执行任务的线程 ID:
class Program
{
static async Task Main(string[] args)
{
foreach (int i in ProduceEvenNumbers(9))
{
ConsoleExt.Write($"{i}-Main");
}
ConsoleExt.Write($"--Main-循环结束");
Console.ReadLine();
}
static IEnumerable<int> ProduceEvenNumbers(int upto)
{
for (int i = 0; i <= upto; i += 2)
{
ConsoleExt.Write($"{i}-ProduceEvenNumbers");
yield return i;
ConsoleExt.Write($"{i}-ProduceEvenNumbers-yielded");
}
ConsoleExt.Write($"--ProduceEvenNumbers-循环结束");
}
}
public static class ConsoleExt
{
public static void Write(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static void WriteLine(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
}
}
输出结果如下,可见整个循环是单线程运行,ProduceEvenNumbers()生产一个,然后Main()就操作一个,Main() 执行一次操作后,线程返回生产线,继续沿着 return 往后执行;生产线循环结束后,Main() 也接着结束:

1.2 yield break:标识迭代中断
如下示例代码,通过条件中断循环:
class Program
{
static void Main()
{
ConsoleExt.Write(string.Join(" ", TakeWhilePositive(new[] { 2, 3, 4, 5, -1, 3, 4 })));
ConsoleExt.Write(string.Join(" ", TakeWhilePositive(new[] { 9, 8, 7 })));
Console.ReadLine();
}
static IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
{
foreach (int n in numbers)
{
if (n > 0) // 遇到负数就中断循环
{
yield return n;
}
else
{
yield break;
}
}
}
}
public static class ConsoleExt
{
public static void Write(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static void WriteLine(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
}
}
输出结果,第一个数组中第五个数为负数,因此至此就中断循环,包括它自己之后的数字不再返回:

1.3 返回类型为 IAsyncEnumerable<T> 的异步迭代器
实际上,不仅可以像前边示例中那样返回类型为 IEnumerable<T>,还可以使用 IAsyncEnumerable<T> 作为迭代器的返回类型,使得迭代器支持异步。
如下示例代码,使用 await foreach 语句对迭代器的结果进行异步迭代:(关于 await foreach 还有另外一个示例可参考 3.2 await foreach() 示例)
class Program
{
public static async Task Main()
{
await foreach (int n in GenerateNumbersAsync(5))
{
ConsoleExt.Write(n);
}
Console.ReadLine();
}
static async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
for (int i = 0; i < count; i++)
{
yield return await ProduceNumberAsync(i);
}
}
static async Task<int> ProduceNumberAsync(int seed)
{
await Task.Delay(1000);
return 2 * seed;
}
}
public static class ConsoleExt
{
public static void Write(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static void WriteLine(object message)
{
Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ");
}
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "));
}
}
输出结果如下,可见输出的结果有不同线程执行:

1.4 迭代器的返回类型可以是 IEnumerator<T> 或 IEnumerator
以下示例代码,通过实现 IEnumerable<T> 接口、GetEnumerator 方法,返回类型为 IEnumerator<T>,来展现 yield 关键字的一个用法:
class Program
{
public static void Main()
{
var ints = new int[] { 1, 2, 3 };
var enumerable = new MyEnumerable<int>(ints);
foreach (var item in enumerable)
{
Console.WriteLine(item);
}
Console.ReadLine();
}
}
public class MyEnumerable<T> : IEnumerable<T>
{
private T[] items;
public MyEnumerable(T[] ts)
{
this.items = ts;
}
public void Add(T item)
{
int num = this.items.Length;
this.items[num + 1] = item;
}
public IEnumerator<T> GetEnumerator()
{
foreach (var item in this.items)
{
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
1.5 不能使用 yield 的情况
- yield return 不能套在 try-catch 中;
- yield break 不能放在 finally 中;

- yield 不能用在带有 in、ref 或 out 参数的方法;
- yield 不能用在 Lambda 表达式和匿名方法;
- yield 不能用在包含不安全的块(unsafe)的方法。
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/yield
二、使用 yield 关键字实现惰性枚举
在 C# 中,可以使用 yield 关键字来实现惰性枚举。惰性枚举是指在使用枚举值时,只有在真正需要时才会生成它们,这可以提高程序的性能,因为在不需要使用枚举值时,它们不会被生成或存储在内存中。
当然对于简单的枚举,实际上还没普通的 List<T> 有优势,因为取枚举值也会对性能有损耗,所以只针对处理大型集合或延迟加载数据才能看到效果。
下面是一个简单示例,展示了如何使用 yield 关键字来实现惰性枚举:
public static IEnumerable<int> enumerableFuc()
{
yield return 1;
yield return 2;
yield return 3;
}
// 使用惰性枚举
foreach (var number in enumerableFuc())
{
Console.WriteLine(number);
}
在上面的示例中,GetNumbers() 方法通过yield关键字返回一个 IEnumerable 对象。当我们使用 foreach 循环迭代这个对象时,每次循环都会调用 MoveNext() 方法,并执行到下一个 yield 语句处,返回一个元素。这样就实现了按需生成枚举的元素,而不需要一次性生成所有元素。
三、通过 IL 代码看 yield 的原理
类比上一章节的示例代码,用 while 循环代替 foreach 循环,发现我们虽然没有实现 GetEnumerator(),也没有实现对应的 IEnumerator 的 MoveNext() 和 Current 属性,但是我们仍然能正常使用这些函数。
static async Task Main(string[] args)
{
// 用 while (enumerator.MoveNext())
// 代替 foreach(int item in enumerableFuc())
IEnumerator<int> enumerator = enumerableFuc().GetEnumerator();
while (enumerator.MoveNext())
{
int current = enumerator.Current;
Console.WriteLine(current);
}
Console.ReadLine();
}
// 一个返回类型为 IEnumerable<int>,其中包含三个 yield return
public static IEnumerable<int> enumerableFuc()
{
Console.WriteLine("enumerableFuc-yield 1");
yield return 1;
Console.WriteLine("enumerableFuc-yield 2");
yield return 2;
Console.WriteLine("enumerableFuc-yield 3");
yield return 3;
}
输出的结果:

下面试着简单看一下 Program 类的源码
源码如下,除了明显的 Main() 和 enumerableFuc() 两个函数外,反编译的时候自动生成了一个新的类 '<enumerableFuc>d__1'。
注:反编译时,语言选择:“IL with C#”,有助于理解。

然后看自动生成的类的实现,发现它继承了 IEnumerable、IEnumerable<T>、IEnumerator、IEnumerator<T>,也实现了MoveNext()、Reset()、GetEnumerator()、Current 属性,这时我们应该可以确认,这个新的类,就是我们虽然没有实现对应的 IEnumerator 的 MoveNext() 和 Current 属性,但是我们仍然能正常使用这些函数的原因了。


然后再具体看下 MoveNext() 函数,根据输出的备注字段,也能清晰的看到迭代过程,下图中紫色部分:

下边是是第三、四次迭代,可以看到行标识可以对得上:

每次调用 MoveNext() 函数都会将“ <>1__state”加 1,一共进行了 4 次迭代,前三次返回 true,最后一次返回 false,代表迭代结束。这四次迭代对应被 3 个 yield return 语句分成4部分的 enumberableFuc() 中的语句。
用 enumberableFuc() 来进行迭代的真实流程就是:
- 运行 enumberableFuc() 函数,获取代码自动生成的类的实例;
- 接着调用 GetEnumberator() 函数,将获取的类自己作为迭代器,准备开始迭代;
- 每次运行 MoveNext() “ <>1__state”增加 1,通过 switch 语句可以让每次调用 MoveNext() 的时候执行不同部分的代码;
- MoveNext() 返回 false,结束迭代。
这也就说明了,yield 关键字其实是一种语法糖,最终还是通过实现 IEnumberable<T>、IEnumberable、IEnumberator<T>、IEnumberator 接口实现的迭代功能。
参考自:c# yield关键字的用法
关于 yield 关键字(C#)的更多相关文章
- 从yield关键字看IEnumerable和Collection的区别
C#的yield关键字由来以久,如果我没有记错的话,应该是在C# 2.0中被引入的.相信大家此关键字的用法已经了然于胸,很多人也了解yield背后的“延迟赋值”机制.但是即使你知道这个机制,你也很容易 ...
- .NET中的yield关键字
浅谈yield http://www.cnblogs.com/qlb5626267/archive/2009/05/08/1452517.html .NET中yield关键字的用法 http://bl ...
- 使用yield关键字让自定义集合实现foreach遍历
一般来说当我们创建自定义集合的时候为了让其能支持foreach遍历,就只能让其实现IEnumerable接口(可能还要实现IEnumerator接口) 但是我们也可以通过使用yield关键字构建的迭代 ...
- C#的yield关键字
using System; using System.Collections.Generic; using System.Reflection; using System.Text.RegularEx ...
- 从range和xrange的性能对比到yield关键字(中)
上节提出了range和xrange的效率问题,这节我们来探究其中的原因 yield的使用 我们看下面的程序: #coding: utf-8 def test(): print 4 print ...
- (转) Python Generators(生成器)——yield关键字
http://blog.csdn.net/scelong/article/details/6969276 生成器是这样一个函数,它记住上一次返回时在函数体中的位置.对生成器函数的第二次(或第 n 次) ...
- 转载yield关键字理解
实现IEnumerable接口及理解yield关键字 [摘要]本文介绍实现IEnumerable接口及理解yield关键字,并讨论IEnumerable接口如何使得foreach语句可以使用. 本 ...
- yield关键字的用法
在上一篇文章中,说了下foreach的用法,但是还是比较复杂的,要实现接口才能进行遍历,有没有简单些的方法呢?答案是肯定的.且看下面. yield关键字的用法: 1.为当前类型添加一个任意方法,但是要 ...
- yield 关键字和迭代器
一般使用方法 yield 关键字向编译器指示它所在的方法是迭代器块 在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值. 这是一个返回值,例如,在 forea ...
- C# 基础小知识之yield 关键字 语法糖
原文地址:http://www.cnblogs.com/santian/p/4389675.html 对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味 ...
随机推荐
- CSS 基础拾遗(核心知识、常见需求)
本篇文章围绕了 CSS 的核心知识点和项目中常见的需求来展开.虽然行文偏长,但较基础,适合初级中级前端阅读,阅读的时候请适当跳过已经掌握的部分. 这篇文章断断续续写了比较久,也参考了许多优秀的文章,但 ...
- 一个WPF开发的、界面简洁漂亮的音频播放器
今天推荐一个界面简洁.美观的.支持国际化开源音频播放器. 项目简介 这是一个基于C# + WPF开发的,界面外观简洁大方,操作体验良好的音频播放器. 支持各种音频格式,包括:MP4.WMA.OGG.F ...
- HTML中link标签的那些属性
在HTML中, link 标签是一个自闭合元素,通常位于文档的 head 部分.它用于建立与外部资源的关联,如样式表.图标等. link 标签具有多个属性,其中 rel 和 href 是最常用的. r ...
- MySQL中的Join 的算法(NLJ、BNL、BKA)
本文已收录至Github,推荐阅读 Java随想录 微信公众号:Java随想录 目录 摘要 什么是Join Index Nested-Loop Join Block Nested-Loop Join ...
- Unity快速接入bugly, 支持Unity2021
鹅厂提供的bugly官方demo工程打包后台也查不到日志,N年不更新(官方已经说不再维护),为此本人做了部分修改测试,提供一个快速接入工程的demo. Unity2021因为版本原因腾讯官方工程不能使 ...
- #PowerBI 1分钟学会,利用format函数,自定义格式显示
PowerBI是一款强大的数据分析和可视化工具,它可以帮助我们快速地创建各种报表和仪表盘,展示数据的洞察和价值. 在PowerBI中,有许多内置的函数可以帮助我们处理和转换数据,其中一个常用的函数就是 ...
- Centos7 部署Django项目 uwsgi + nginx
启动 首先确保你的django项目是可以在虚拟环境中跑起来的,环境管理窝用的是pyenv,pyenv不知道什么东西的可以参考窝之前写过的Pyenv环境管理的安装文. 项目启动 python manag ...
- 微信小程序客服、支付、定位、下拉加载功能
一.客服功能 1.只要你微信小程序,后台添加了客服,引用以下button,就可以进入聊天(在小程序官网如何添加客服用户,请自行百度,谢谢) 2.通过按钮方式 <button open-type= ...
- 2021-07-17:一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度。比如, {3,1,2,4,5}、{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山。山峰A和山
2021-07-17:一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度.比如, {3,1,2,4,5}.{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山.山峰A和山 ...
- Spring源码:Bean生命周期(四)
前言 在之前的文章中,我们介绍了 Bean 的核心概念.Bean 定义的解析过程以及 Bean 创建的准备工作.在今天的文章中,我们将深入探讨 Bean 的创建过程,并主要讲解 createBean ...