zoukankan      html  css  js  c++  java
  • [C#.NET 拾遗补漏]:迭代器和列举器

    大家好,这是 [C#.NET 拾遗补漏] 系列的第 07 篇文章。

    在 C# 中,大多数方法都是通过 return 语句立即把程序的控制权交回给调用者,同时也会把方法内的本地资源释放掉。而包含 yield 语句的方法则允许在依次返回多个值给调用者的期间保留本地资源,等所有值都返回结束时再释放掉本来资源,这些返回的值形成一组序列被调用者使用。在 C# 中,这种包含 yield 语句的方法、属性或索引器就是迭代器。

    迭代器中的 yield 语句分为两种:

    • yeild return,把程序控制权交回调用者并保留本地状态,调用者拿到返回的值继续往后执行。
    • yeild break,用于告诉程序当前序列已经结束,相当于正常代码块的 return 语句(迭代器中直接使用 return 是非法的)。

    下面是一个用来生成斐波纳契序列的迭代器示例:

    IEnumerable<int> Fibonacci(int count)
    {
      int prev = 1;
      int curr = 1;
      for (int i = 0; i < count; i++)
      {
        yield return prev;
        int temp = prev + curr;
        prev = curr;
        curr = temp;
      }
    }
    
    void Main()
    {
      foreach (int term in Fibonacci(10))
      {
        Console.WriteLine(term);
      }
    }
    

    输出:

    1
    1
    2
    3
    5
    8
    13
    21
    34
    55
    

    实际场景中,我们一般很少直接写迭代器,因为大部分需要迭代的场景都是数组、集合和列表,而这些类型内部已经封装好了所需的迭代器。比如 C# 中的数组之所以可以被遍历是因为它实现了 IEnumerable 接口,通过 GetEnumerator() 方法可以获得数组的列举器 Enumerator,而该列举器就是通过迭代器来实现的。比如最常见的一种使用场景就是遍历数组中的每一个元素,如下面逐个打印数组元素的示例。

    int[] numbers = { 1, 2, 3, 4, 5 };
    IEnumerator enumerator = numbers.GetEnumerator();
    while (enumerator.MoveNext())
    {
      Console.WriteLine(enumerator.Current);
    }
    

    其实这就是 foreach 的工作原理,上面代码可以用 foreach 改写如下:

    int[] numbers = { 1, 2, 3, 4, 5 };
    foreach (int number in numbers)
    {
      Console.WriteLine(number);
    }
    

    当然,列举器不一定非要通过迭代器实现,例如下面这个自定义的列举器 CoffeeEnumerator。

    public class CoffeeCollection : IEnumerable
    {
      private CoffeeEnumerator enumerator;
      public CoffeeCollection()
      {
        enumerator = new CoffeeEnumerator();
      }
    
      public IEnumerator GetEnumerator()
      {
        return enumerator;
      }
    
      public class CoffeeEnumerator : IEnumerator
      {
        string[] items = new string[3] { "espresso", "macchiato", "latte" };
        int currentIndex = -1;
        public object Current
        {
          get
          {
            return items[currentIndex];
          }
        }
        public bool MoveNext()
        {
          currentIndex++;
          if (currentIndex < items.Length)
          {
            return true;
          }
          return false;
        }
        public void Reset()
        {
          currentIndex = 0;
        }
      }
    }
    

    使用:

    public static void Main(string[] args)
    {
      foreach (var coffee in new CoffeeCollection())
      {
        Console.WriteLine(coffee);
      }
    }
    

    理解迭代器和列举器可以帮助我们写出更高效的代码。比如判断一个 IEnumerable<T> 对象是否包含元素,经常看到有些人这么写:

    if(enumerable.Count() > 0)
    {
      // 集合中有元素
    }
    

    但如果用列举器的思维稍微思考一下就知道,Count() 为了获得集合元素数量必然要迭代完所有元素,时间复杂度为 O(n)。而仅仅是要知道集合中是否包含元素,其实迭代一次就可以了。所以效率更好的做法是:

    if(enumerable.GetEnumerator().MoveNext())
    {
      // 集合中有元素
    }
    

    这样写时间复杂度是 O(1),效率显然更高。为了书写方便,C# 提供了扩展方法 Any()

    if(enumerable.Any())
    {
      // 集合中有元素
    }
    

    所以如有需要,应尽可能使用 Any 方法,效率更高。

    再比如在 EF Core 中,需要执行 IQueryable<T> 查询时,有时候使用 AsEnumerable() 比使用 ToList、ToArray 等更高效,因为 ToList、ToArray 等会立即执行列举操作,而 AsEnumerable() 可以把列举操作延迟到真正被需要的时候再执行。当然也要考虑实际应用场景,Array、List 等更方便调用者使用,特别是要获取元素总数量、增删元素等这种操作。

    **********转摘:https://www.cnblogs.com/willick/p/13473526.html

  • 相关阅读:
    UVa532 Dungeon Master 三维迷宫
    6.4.2 走迷宫
    UVA 439 Knight Moves
    UVa784 Maze Exploration
    UVa657 The die is cast
    UVa572 Oil Deposits DFS求连通块
    UVa10562 Undraw the Trees
    UVa839 Not so Mobile
    327
    UVa699 The Falling Leaves
  • 原文地址:https://www.cnblogs.com/linybo/p/13889763.html
Copyright © 2011-2022 走看看