关于 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 关键字,则意味 ...
随机推荐
- django使用多个数据库实现
一.说明: 在开发 Django 项目的时候,很多时候都是使用一个数据库,即 settings 中只有 default 数据库,但是有一些项目确实也需要使用多个数据库,这样的项目,在数据库配置和使用的 ...
- 笔记:linux必备网络基础概念和以太网技术基础
笔记:linux必备网络基础概念和以太网技术基础 由于后面可能需要对交换机进行一些配置,所以也是临时学习一下iptables的相关配置,以及一些基本的网络知识,下面就是我看到一些资料做的一些总结,希望 ...
- Flutter(十) 音频+视频播放
在Flutter中,我们有各种插件可供使用,从而实现音频和视频的播放功能. 例如,可以使用"text_to_speech"插件来将文字转换为语音,使用内置的"video_ ...
- 文心一言 VS chatgpt (16)-- 算法导论4.1 1~2题
一.当A的所有元素均为负数时,FIND-MAXIMUM-SUBARRAY返回什么? 文心一言: 当A的所有元素均为负数时,FIND-MAXIMUM-SUBARRAY返回-1. 这是因为在二进制中,当所 ...
- 2022-08-21:以下go语言代码输出什么?A:0;B:panic;C:不知道。 package main var n = -99 func main() { m := make(map[
2022-08-21:以下go语言代码输出什么?A:0:B:panic:C:不知道. package main var n = -99 func main() { m := make(map[stri ...
- 2021-01-17:java中,HashMap底层数据结构是什么?
福哥答案2020-01-07: 1.7 数组+链表重要字段://HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂,至于为什么这么做,后面会有详 ...
- Selenium - 快速上手之启动浏览器
Selenium - 浏览器启动/关闭 使用webdriver前,需要先导入包 from selenium import webdriver,每次打开浏览器时; 执行完毕要记得关闭浏览器,使用 dri ...
- Linux基础 | 青训营笔记
课程介绍 以下是Linux系统的相关知识(但是并不全部出现在本文中) 学习Linux的价值 Liux是现代化应用程序交付的首选平台,无论是部署在裸机.虚拟化还是容器化环境 公司内部服务(TCE.Faa ...
- Structured Streaming 的异常处理 【Concurrent update to the log. Multiple streaming jobs detected】
目录 异常信息 一.异常表象原因 1.异常源码: 2.打个断点 二.解决方案 1.可以通过代码指定各分区的开始offset 2.不删除而是更改checkpoint offset下的批次文件 三.异常背 ...
- ChatGPT 推出 iOS 应用,支持语音输入,使用体验如何?
最近,OpenAI 宣布推出官方 iOS 应用,允许用户随时随地访问其高人气 AI 聊天机器人,此举也打破了近几个月内苹果 App Store 上充斥似是而非的山寨服务的窘境. 该应用程序是 Chat ...