〇、前言

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#)的更多相关文章

  1. 从yield关键字看IEnumerable和Collection的区别

    C#的yield关键字由来以久,如果我没有记错的话,应该是在C# 2.0中被引入的.相信大家此关键字的用法已经了然于胸,很多人也了解yield背后的“延迟赋值”机制.但是即使你知道这个机制,你也很容易 ...

  2. .NET中的yield关键字

    浅谈yield http://www.cnblogs.com/qlb5626267/archive/2009/05/08/1452517.html .NET中yield关键字的用法 http://bl ...

  3. 使用yield关键字让自定义集合实现foreach遍历

    一般来说当我们创建自定义集合的时候为了让其能支持foreach遍历,就只能让其实现IEnumerable接口(可能还要实现IEnumerator接口) 但是我们也可以通过使用yield关键字构建的迭代 ...

  4. C#的yield关键字

    using System; using System.Collections.Generic; using System.Reflection; using System.Text.RegularEx ...

  5. 从range和xrange的性能对比到yield关键字(中)

    上节提出了range和xrange的效率问题,这节我们来探究其中的原因   yield的使用   我们看下面的程序: #coding: utf-8 def test(): print 4 print ...

  6. (转) Python Generators(生成器)——yield关键字

    http://blog.csdn.net/scelong/article/details/6969276 生成器是这样一个函数,它记住上一次返回时在函数体中的位置.对生成器函数的第二次(或第 n 次) ...

  7. 转载yield关键字理解

    实现IEnumerable接口及理解yield关键字   [摘要]本文介绍实现IEnumerable接口及理解yield关键字,并讨论IEnumerable接口如何使得foreach语句可以使用. 本 ...

  8. yield关键字的用法

    在上一篇文章中,说了下foreach的用法,但是还是比较复杂的,要实现接口才能进行遍历,有没有简单些的方法呢?答案是肯定的.且看下面. yield关键字的用法: 1.为当前类型添加一个任意方法,但是要 ...

  9. yield 关键字和迭代器

    一般使用方法     yield 关键字向编译器指示它所在的方法是迭代器块 在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值. 这是一个返回值,例如,在 forea ...

  10. C# 基础小知识之yield 关键字 语法糖

    原文地址:http://www.cnblogs.com/santian/p/4389675.html 对于yield关键字我们首先看一下msdn的解释: 如果你在语句中使用 yield 关键字,则意味 ...

随机推荐

  1. JUC同步工具CountDownLatch

    CountDownLatch:允许一条或多条线程等待其它线程中的一组操作完成后再继续执行. 在探究CountDownLatch之前,我们知道Thread的join也有类似功能,先看thread的joi ...

  2. java生成机器码

    java根据系统参数生成每个计算机的唯一标识. 1. 获取CPU序列号 /** * 获取CPU序列号 * @return * @throws IOException */ public static ...

  3. Gradio入门到进阶全网最详细教程[二]:快速搭建AI算法可视化部署演示(侧重参数详解和案例实践)

    Gradio入门到进阶全网最详细教程[二]:快速搭建AI算法可视化部署演示(侧重参数详解和案例实践) 相关文章:Gradio入门到进阶全网最详细教程[一]:快速搭建AI算法可视化部署演示(侧重项目搭建 ...

  4. 数据分析04-pandas(apply函数、排序、数据合、分组聚合、透视表、交叉表及项目分析)

    数据分析-04 排序 按标签(行)排序 按标签(列)排序 按某列值排序 数据合并 concat merge & join 分组聚合 分组 聚合 透视表与交叉表 透视表 交叉表 项目:分析影响学 ...

  5. 【机器学习与深度学习理论要点】20. 什么是激活函数,为什么要用激活函数,常见的激活函数和特点,softmax函数

    1)什么是激活函数,为什么要用激活函数? 激活函数,指神经网络中将输入信号的总和转换为输出信号的函数,激活函数将多层感知机输出转换为非线性,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应 ...

  6. 几行代码教你快速创建scrapy项目,非常实用建议收藏!

    import shutil,os修改settings.py def config(scrapy_path,project_name): judge=input("是否自动修改配置?是:yes ...

  7. ReactHub:我用 ChatGPT 搞了一个 React 的资源导航网站,谁有我用心啊!

    大家好,我是DOM哥. 图谱年年有,今年我来盘! 之前已经盘完了 Vue 的技术图谱,今天来盘 React 的. 我用 ChatGPT 开发了一个 React 的资源导航网站. 不管你是资深 Reac ...

  8. 【Azure 存储服务】使用 AppendBlobClient 对象实现对Blob进行追加内容操作

    问题描述 在Azure Blob的官方示例中,都是对文件进行上传到Blob操作,没有实现对已创建的Blob进行追加的操作.如果想要实现对一个文件的多次追加操作,每一次写入的时候,只传入新的内容? 问题 ...

  9. C# POST提交以及 解析 JSON 实例

    一.解析的JSON字符串如下 {"tinyurl":"http:\/\/dwz.cn\/v9BxE","status":0,"lo ...

  10. 2021-11-15:四数相加 II。给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:0 <= i,

    2021-11-15:四数相加 II.给你四个整数数组 nums1.nums2.nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:0 <= ...