首页 > 编程语言 >关于 yield 关键字【C# 基础】

关于 yield 关键字【C# 基础】

时间:2024-10-16 15:02:05浏览次数:8  
标签:Console 迭代 C# yield 关键字 static return public

关于 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关键字的用法

本文来自博客园,作者:橙子家,欢迎微信扫码关注博主【橙子家czzj】,有任何疑问欢迎沟通,共同成长!

转载本文请注明原文链接:https://www.cnblogs.com/hnzhengfy/p/yield.html

  分类: C# 基础

标签:Console,迭代,C#,yield,关键字,static,return,public
From: https://www.cnblogs.com/sexintercourse/p/18469943

相关文章

  • [整理]C#反射(Reflection)详解
    [整理]C#反射(Reflection)详解本人理解:装配件:Assembly(程序集)晚绑定:后期绑定MSDN:反射(C#编程指南)-----------------原文如下--------1、什么是反射2、命名空间与装配件的关系3、运行期得到类型信息有什么用4、如何使用反射获取类型5、如何根据类型来动态创建对象6、......
  • YT to WAV - A Handy Tool for Audio Conversion
    Intoday'sdigitalworld,weoftenhavetheneedtoconvertaudioformats.Maybeyou'vecomeacrossagreatsongoraninterestingspeechonYouTubeandwantedtosaveitinadifferentaudioformatforvariousreasons.Well,that'swhereY......
  • yield 语句 - 提供下一个元素
    yield语句-提供下一个元素项目2024/10/153个参与者反馈本文内容迭代器的执行C#语言规范另请参阅在迭代器中使用 yield 语句提供下一个值或表示迭代结束。 yield 语句有以下两种形式:yieldreturn:在迭代中提供下一个值,如以下示例所示:C#复制 运行f......
  • python批处理,一键打开vscode窗口,分别加载jeecg前后端项目,并运行前后端服务.
    importsubprocess#VsCode打开后端项目cmd1=["code","D:\pro\JeecgBoot-v3.7.1\jeecg-boot"]process1=subprocess.Popen(cmd1,stdout=subprocess.PIPE,shell=True)output1,_=process1.communicate()print(output1.decode('utf-8')......
  • 【LRC2024】桌面图像编辑和管理软件和安装步骤(附百度云安装包)
    目录一、软件简介1.软件概述2.主要功能二、系统要求1.最低系统要求2.推荐系统要求三、安装步骤1.获取安装包2.运行安装程序Windows系统Mac系统3.完成安装Windows系统Mac系统一、软件简介1.软件概述AdobeLightroomClassic(简称LRC)是一款专为数码摄......
  • C语言手撕实战代码_线索二叉树_先序中序线索二叉树_树的先根遍历_后根遍历_树的度_孩
    文章目录1.设计算法构造一棵先序线索二叉树2.先序线索二叉树的先序遍历算法3.设计算法构造一棵中序线索二叉树4.遍历中序线索二叉树5.树的先根遍历和后根遍历6.树T的叶子结点个数7.计算一棵以孩子兄弟表示的树T的度,该算法的时间复杂度为O(n)8.计算树孩子兄弟链表表示的T......
  • 前端新手教程:HTML、CSS 和 JavaScript 全面详解及实用案例
    一、引言在当今数字化的时代,前端开发扮演着至关重要的角色,它决定了用户与网页和应用程序交互的体验。HTML、CSS和JavaScript作为前端开发的核心技术,分别负责网页的结构、样式和交互。本教程将为前端新手全面深入地介绍HTML、CSS和JavaScript的知识点,并通过实用案例帮助......
  • OS-Nachos内存管理
    实验目的在Nachos现有页表的基础上,增加TLB快表机制,使得在做虚拟地址到物理地址的转换时,优先从TLB快表中读取;针对TLB增加NRU置换算法;除实验源码和实验结果截图以外,需提供以下文字解释说明:解释说明nachos-xuserProgName的启动过程及原生Nachos系统中的内存访问过程;解释说明......
  • 【c#】我们为什么要用 yield return
    【c#】我们为什么要用yieldreturn明尼苏达的微笑嗯嗯,你说的都对​关注他 43人赞同了该文章声明:本文部分内容来自《moreeffectivec#》,特此声明c#里面的yieldreturn没什么高大上的,就是一个一个的return。但是我们不能小看这种用法,他能给编......
  • 文件同步文件备份软件 Goodsync 序列号
    GoodSync是一种简单和可靠的文件备份和文件同步软件。它会自动分析、同步,并备份您的电子邮件、珍贵的家庭照片、联系人,、MP3歌曲,财务文件和其他重要文件本地-之间的台式机,笔记本电脑,服务器,外部驱动器,以及WindowsMobile设备,以及通过FTP远程,网友的WebDAV等等。该版本已内置序......