翼度科技»论坛 编程开发 .net 查看内容

由C# yield return引发的思考

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
前言

    当我们编写 C# 代码时,经常需要处理大量的数据集合。在传统的方式中,我们往往需要先将整个数据集合加载到内存中,然后再进行操作。但是如果数据集合非常大,这种方式就会导致内存占用过高,甚至可能导致程序崩溃。
    C# 中的yield return机制可以帮助我们解决这个问题。通过使用yield return,我们可以将数据集合按需生成,而不是一次性生成整个数据集合。这样可以大大减少内存占用,并且提高程序的性能。
    在本文中,我们将深入讨论 C# 中yield return的机制和用法,帮助您更好地理解这个强大的功能,并在实际开发中灵活使用它。
使用方式

上面我们提到了yield return将数据集合按需生成,而不是一次性生成整个数据集合。接下来通过一个简单的示例,我们看一下它的工作方式是什么样的,以便加深对它的理解
  1. foreach (var num in GetInts())
  2. {
  3.     Console.WriteLine("外部遍历了:{0}", num);
  4. }
  5. IEnumerable<int> GetInts()
  6. {
  7.     for (int i = 0; i < 5; i++)
  8.     {
  9.         Console.WriteLine("内部遍历了:{0}", i);
  10.         yield return i;
  11.     }
  12. }
复制代码
首先,在GetInts方法中,我们使用yield return关键字来定义一个迭代器。这个迭代器可以按需生成整数序列。在每次循环时,使用yield return返回当前的整数。通过1foreach循环来遍历 GetInts方法返回的整数序列。在迭代时GetInts方法会被执行,但是不会将整个序列加载到内存中。而是在需要时,按需生成序列中的每个元素。在每次迭代时,会输出当前迭代的整数对应的信息。所以输出的结果为
  1. 内部遍历了:0
  2. 外部遍历了:0
  3. 内部遍历了:1
  4. 外部遍历了:1
  5. 内部遍历了:2
  6. 外部遍历了:2
  7. 内部遍历了:3
  8. 外部遍历了:3
  9. 内部遍历了:4
  10. 外部遍历了:4
复制代码
可以看到,整数序列是按需生成的,并且在每次生成时都会输出相应的信息。这种方式可以大大减少内存占用,并且提高程序的性能。当然从c# 8开始异步迭代的方式同样支持
  1. await foreach (var num in GetIntsAsync())
  2. {
  3.     Console.WriteLine("外部遍历了:{0}", num);
  4. }
  5. async IAsyncEnumerable<int> GetIntsAsync()
  6. {
  7.     for (int i = 0; i < 5; i++)
  8.     {
  9.         await Task.Yield();
  10.         Console.WriteLine("内部遍历了:{0}", i);
  11.         yield return i;
  12.     }
  13. }
复制代码
和上面不同的是,如果需要用异步的方式,我们需要返回IAsyncEnumerable类型,这种方式的执行结果和上面同步的方式执行的结果是一致的,我们就不做展示了。上面我们的示例都是基于循环持续迭代的,其实使用yield return的方式还可以按需的方式去输出,这种方式适合灵活迭代的方式。如下示例所示
  1. foreach (var num in GetInts())
  2. {
  3.     Console.WriteLine("外部遍历了:{0}", num);
  4. }
  5. IEnumerable<int> GetInts()
  6. {
  7.     Console.WriteLine("内部遍历了:0");
  8.     yield return 0;
  9.     Console.WriteLine("内部遍历了:1");
  10.     yield return 1;
  11.     Console.WriteLine("内部遍历了:2");
  12.     yield return 2;
  13. }
复制代码
foreach循环每次会调用GetInts()方法,GetInts()方法的内部便使用yield return关键字返回一个结果。每次遍历都会去执行下一个yield return。所以上面代码输出的结果是
  1. 内部遍历了:0
  2. 外部遍历了:0
  3. 内部遍历了:1
  4. 外部遍历了:1
  5. 内部遍历了:2
  6. 外部遍历了:2
复制代码
探究本质

上面我们展示了yield return如何使用的示例,它是一种延迟加载的机制,它可以让我们逐个地处理数据,而不是一次性地将所有数据读取到内存中。接下来我们就来探究一下神奇操作的背后到底是如何实现的,方便让大家更清晰的了解迭代体系相关。
foreach本质

首先我们来看一下foreach为什么可以遍历,也就是如果可以被foreach遍历的对象,被遍历的操作需要满足哪些条件,这个时候我们可以反编译工具来看一下编译后的代码是什么样子的,相信大家最熟悉的就是List集合的遍历方式了,那我们就用List的示例来演示一下
  1. List<int> ints = new List<int>();
  2. foreach(int item in ints)
  3. {
  4.     Console.WriteLine(item);
  5. }
复制代码
上面的这段代码很简单,我们也没有给它任何初始化的数据,这样可以排除干扰,让我们能更清晰的看到反编译的结果,排除其他干扰。它反编译后的代码是这样的
  1. List<int> list = new List<int>();
  2. List<int>.Enumerator enumerator = list.GetEnumerator();
  3. try
  4. {
  5.     while (enumerator.MoveNext())
  6.     {
  7.         int current = enumerator.Current;
  8.         Console.WriteLine(current);
  9.     }
  10. }
  11. finally
  12. {
  13.     ((IDisposable)enumerator).Dispose();
  14. }
复制代码
可以反编译代码的工具有很多,我用的比较多的一般是ILSpy、dnSpy、dotPeek和在线c#反编译网站sharplab.io,其中dnSpy还可以调试反编译的代码。
通过上面的反编译之后的代码我们可以看到foreach会被编译成一个固定的结构,也就是我们经常提及的设计模式中的迭代器模式结构
  1. Enumerator enumerator = list.GetEnumerator();
  2. while (enumerator.MoveNext())
  3. {
  4.    var current = enumerator.Current;
  5. }
复制代码
通过这段固定的结构我们总结一下foreach的工作原理

  • 可以被foreach的对象需要要包含GetEnumerator()方法
  • 迭代器对象包含MoveNext()方法和Current属性
  • MoveNext()方法返回bool类型,判断是否可以继续迭代。Current属性返回当前的迭代结果。
我们可以看一下List类可迭代的源码结构是如何实现的
  1. public class List<T> : IList<T>, IList, IReadOnlyList<T>
  2. {
  3.     public Enumerator GetEnumerator() => new Enumerator(this);
  4.     IEnumerator<T> IEnumerable<T>.GetEnumerator() => Count == 0 ? SZGenericArrayEnumerator<T>.Empty : GetEnumerator();
  5.     IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<T>)this).GetEnumerator();
  6.     public struct Enumerator : IEnumerator<T>, IEnumerator
  7.     {
  8.         public T Current => _current!;
  9.         public bool MoveNext()
  10.         {
  11.         }
  12.     }
  13. }
复制代码
这里涉及到了两个核心的接口IEnumerable

举报 回复 使用道具