前言
当我们编写 C# 代码时,经常需要处理大量的数据集合。在传统的方式中,我们往往需要先将整个数据集合加载到内存中,然后再进行操作。但是如果数据集合非常大,这种方式就会导致内存占用过高,甚至可能导致程序崩溃。
C# 中的yield return机制可以帮助我们解决这个问题。通过使用yield return,我们可以将数据集合按需生成,而不是一次性生成整个数据集合。这样可以大大减少内存占用,并且提高程序的性能。
在本文中,我们将深入讨论 C# 中yield return的机制和用法,帮助您更好地理解这个强大的功能,并在实际开发中灵活使用它。
使用方式
上面我们提到了yield return将数据集合按需生成,而不是一次性生成整个数据集合。接下来通过一个简单的示例,我们看一下它的工作方式是什么样的,以便加深对它的理解- foreach (var num in GetInts())
- {
- Console.WriteLine("外部遍历了:{0}", num);
- }
- IEnumerable<int> GetInts()
- {
- for (int i = 0; i < 5; i++)
- {
- Console.WriteLine("内部遍历了:{0}", i);
- yield return i;
- }
- }
复制代码 首先,在GetInts方法中,我们使用yield return关键字来定义一个迭代器。这个迭代器可以按需生成整数序列。在每次循环时,使用yield return返回当前的整数。通过1foreach循环来遍历 GetInts方法返回的整数序列。在迭代时GetInts方法会被执行,但是不会将整个序列加载到内存中。而是在需要时,按需生成序列中的每个元素。在每次迭代时,会输出当前迭代的整数对应的信息。所以输出的结果为- 内部遍历了:0
- 外部遍历了:0
- 内部遍历了:1
- 外部遍历了:1
- 内部遍历了:2
- 外部遍历了:2
- 内部遍历了:3
- 外部遍历了:3
- 内部遍历了:4
- 外部遍历了:4
复制代码 可以看到,整数序列是按需生成的,并且在每次生成时都会输出相应的信息。这种方式可以大大减少内存占用,并且提高程序的性能。当然从c# 8开始异步迭代的方式同样支持- await foreach (var num in GetIntsAsync())
- {
- Console.WriteLine("外部遍历了:{0}", num);
- }
- async IAsyncEnumerable<int> GetIntsAsync()
- {
- for (int i = 0; i < 5; i++)
- {
- await Task.Yield();
- Console.WriteLine("内部遍历了:{0}", i);
- yield return i;
- }
- }
复制代码 和上面不同的是,如果需要用异步的方式,我们需要返回IAsyncEnumerable类型,这种方式的执行结果和上面同步的方式执行的结果是一致的,我们就不做展示了。上面我们的示例都是基于循环持续迭代的,其实使用yield return的方式还可以按需的方式去输出,这种方式适合灵活迭代的方式。如下示例所示- foreach (var num in GetInts())
- {
- Console.WriteLine("外部遍历了:{0}", num);
- }
- IEnumerable<int> GetInts()
- {
- Console.WriteLine("内部遍历了:0");
- yield return 0;
- Console.WriteLine("内部遍历了:1");
- yield return 1;
- Console.WriteLine("内部遍历了:2");
- yield return 2;
- }
复制代码 foreach循环每次会调用GetInts()方法,GetInts()方法的内部便使用yield return关键字返回一个结果。每次遍历都会去执行下一个yield return。所以上面代码输出的结果是- 内部遍历了:0
- 外部遍历了:0
- 内部遍历了:1
- 外部遍历了:1
- 内部遍历了:2
- 外部遍历了:2
复制代码 探究本质
上面我们展示了yield return如何使用的示例,它是一种延迟加载的机制,它可以让我们逐个地处理数据,而不是一次性地将所有数据读取到内存中。接下来我们就来探究一下神奇操作的背后到底是如何实现的,方便让大家更清晰的了解迭代体系相关。
foreach本质
首先我们来看一下foreach为什么可以遍历,也就是如果可以被foreach遍历的对象,被遍历的操作需要满足哪些条件,这个时候我们可以反编译工具来看一下编译后的代码是什么样子的,相信大家最熟悉的就是List集合的遍历方式了,那我们就用List的示例来演示一下- List<int> ints = new List<int>();
- foreach(int item in ints)
- {
- Console.WriteLine(item);
- }
复制代码 上面的这段代码很简单,我们也没有给它任何初始化的数据,这样可以排除干扰,让我们能更清晰的看到反编译的结果,排除其他干扰。它反编译后的代码是这样的- List<int> list = new List<int>();
- List<int>.Enumerator enumerator = list.GetEnumerator();
- try
- {
- while (enumerator.MoveNext())
- {
- int current = enumerator.Current;
- Console.WriteLine(current);
- }
- }
- finally
- {
- ((IDisposable)enumerator).Dispose();
- }
复制代码可以反编译代码的工具有很多,我用的比较多的一般是ILSpy、dnSpy、dotPeek和在线c#反编译网站sharplab.io,其中dnSpy还可以调试反编译的代码。
通过上面的反编译之后的代码我们可以看到foreach会被编译成一个固定的结构,也就是我们经常提及的设计模式中的迭代器模式结构- Enumerator enumerator = list.GetEnumerator();
- while (enumerator.MoveNext())
- {
- var current = enumerator.Current;
- }
复制代码 通过这段固定的结构我们总结一下foreach的工作原理
- 可以被foreach的对象需要要包含GetEnumerator()方法
- 迭代器对象包含MoveNext()方法和Current属性
- MoveNext()方法返回bool类型,判断是否可以继续迭代。Current属性返回当前的迭代结果。
我们可以看一下List类可迭代的源码结构是如何实现的- public class List<T> : IList<T>, IList, IReadOnlyList<T>
- {
- public Enumerator GetEnumerator() => new Enumerator(this);
-
- IEnumerator<T> IEnumerable<T>.GetEnumerator() => Count == 0 ? SZGenericArrayEnumerator<T>.Empty : GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<T>)this).GetEnumerator();
- public struct Enumerator : IEnumerator<T>, IEnumerator
- {
- public T Current => _current!;
- public bool MoveNext()
- {
- }
- }
- }
复制代码 这里涉及到了两个核心的接口IEnumerable |