首页 > 编程语言 >关于 yield 关键字(C#)

关于 yield 关键字(C#)

时间:2023-07-24 21:44:40浏览次数:36  
标签:Console 迭代 C# yield 关键字 static return public

阅读目录


回到顶部

〇、前言

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

 

 

出处:https://www.cnblogs.com/czzj/p/yield.html

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

相关文章

  • CN65 极致性价比小主机黑苹果的折腾之旅(后续修改,完美版本)
    针对第一篇文章中的一些问题怎么解决的:第一、网卡的mac地址,如图修复第二、蓝牙的修复,其实这个也是玄学博通的943224pciebt2网卡正确的蓝牙驱驱动  而错误的蓝牙usb主机控制器里的“版本显示是低于1.56的,我的是显示版本:0.42”无论用什么修改方法就是失败,原因就在这里。......
  • 超级好用的绕过php的disable_functions
    寻思寻思今天就写了吧这里背景是在打同学搭的网站,一句话已经进去了,但是执行不了命令 能够看到能用的函数几乎都被禁了在网上找了挺多方法都用不了,蚁剑的各种插件也绕不过最后找到了这个 哥斯拉的绕过disable中的双链表它的说法是这样的 (来源:http://www.hackdig.com/......
  • fix: fix: Apple Watch无法解锁MBP
    TLDRnozuonodie.尽量别修改用了挺久的设备名...前言最近突然发现,AppleWatch不能解锁Macbook了参考网上的帖子,经过N次重启,总算解决问题了...解决步骤钥匙串访问(keychain)打开钥匙串访问可以用spotlight搜索“钥匙串访问“或者“keychain”修改显示方式......
  • CSP-J 济南刷题训练营
    Day1:基础算法枚举从可能得集合中一一尝试统计贡献。模拟模拟题目中要求的操作NOIP2014生活大爆炸版石头剪刀布洛谷链接:P1328[NOIP2014提高组]生活大爆炸版石头剪刀布注意到赢了是得\(1\)分,平局和输都是\(0\)分,所以我们直接根据题意打表。intVs[5][5]={{0,0,1,1,......
  • kubectl - 如何列出Pod中运行的所有容器,包括初始化容器
    初始化容器存储在spec.initContainers中:kubectlgetpodsPOD_NAME_HERE-ojsonpath={.spec.initContainers[*].name}运行的所有容器在containers中kubectlgetpodsPOD_NAME_HERE-ojsonpath={.spec.containers[*].name}可以使用JSONPathmagic来显示两者kubectlgetpo......
  • ORACLE空间管理实验4:块管理之ASSM三级位图结构
    L1、L2、L3块的作用:--方便查找数据块。L3中有指向L2的指针,L2有指向L1的指针,L1中有多个数据块的指针和状态。1、每个L3中,有多个L2的地址(第一个L3是段头)。2、每个L2中,有多个L1的地址。3、每个L1中,有多个数据块地址。ORACLE最多支持三级位图。一级位图用于管理具体数据块的使用。......
  • ORACLE空间管理实验5:块管理之ASSM下高水位的影响--删除和查询
    高水位概念:所有的oracle段(segments,在此,为了理解方便,建议把segment作为表的一个同义词)都有一个在段内容纳数据的上限,我们把这个上限称为"highwatermark"或HWM。这个HWM是一个标记,用来说明已经有多少没有使用的数据块分配给这个segment。HWM原则上HWM只......
  • ORACLE空间管理实验2:区的管理与分配
    内容基于LMT管理的表空间,字典管理已经不用了。本篇主要验证了这些问题:1.LMT管理的表空间,区的分配有两种方法:系统分配和UNIFORM固定大小-->见实验   2.验证Oracle找寻可用区的方式:从数据文件开头的位图块中获得可用区的信息,DUMP时可见FIRST:3这种,表......
  • IMU模式下DML语句所产生的REDO RECORD格式解读
    总结:IMU模式下DML语句所产生的REDORECORD格式,是先有操作的changerector,再有向向UNDO段头的事务表写事务信息的changerector,再提交操作的changerector后,才进行把数据修改前值放到UNDO的changerector。注意:实验中INSERT和DELETE是先后做的,UPDATE操作是......
  • 深入解析Oracle IMU模式下的REDO格式
    1.什么是IMU?IMU的主要作用是什么,也就是说为了解决什么问题?IMU--->InMemoryUndo,10g新特性,数据库会在sharedpool开辟独立的内存区域用于存储Undo信息,每个新事务都会分配一个IMUbuffer(私有的),一个buffer里有很多node,一个node相当于一个block(回滚块)。IMU特性:IMU顾名思义就是在内......