C#中的IEnumerator 和 yield

目录

1.非泛型的IEnumerator 和 IEnumerable 的实现:

2.C#1举例如何实现可迭代对象:

3.C#2举例实现:使用yield关键字简化

4.yield break 相当于普通方法中的return

5.finally块 被 实现为 Dispose

6.实际开发中实现迭代器的例子

参考:


.NET中  通过 IEnumerator 和 IEnumerable 以及他们的泛型等价物 来封装 Iterator 模式。

迭代器模式:允许你访问一个数据项序列,而无需关心序列内部的组织结构


1.非泛型的IEnumerator 和 IEnumerable 的实现

//  枚举器接口 IEnumerator
public interface IEnumerator
{
    object Current { get; }
    // 如果是返回 false,就是结束迭代器块
    bool MoveNext();
    void Reset();
}
//  可枚举的接口 IEnumerable, 返回枚举器接口
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

在上述两个接口在foreach中的运用,foreach其实是GoF Iterator模式的实现。

foreach(var item in dict)

或者
// dict是实现了 IEnumerable 接口的可迭代对象。如果不用foreach,可以像下面这样迭代。
var iterator = dict.GetEnumerator();
while(iterator.MoveNext())
{
  item = iterator.Current;
  .....
}

2.C#1举例如何实现可迭代对象:

IterationSample 类为可迭代对象,需要继承IEnumerable接口,需要实现:

    一个获得枚举器的函数,GetEnumerator函数

    一个枚举器类,IterationSampleIterator

class IterationSample : IEnumerable
{
    object[] values;
    int startingPoint;

    public IterationSample(object[] values, int startingPoint)
    {
        this.values = values;
        this.startingPoint = startingPoint;
    }

    public IEnumerator GetEnumerator()
    {
        return new IterationSampleIterator(this);
    }
    // 迭代器实现
    class IterationSampleIterator : IEnumerator
    {
        // 重要数据成员,来获取当前值,下一个值,是否结束。在构造函数中赋值。
        IterationSample parent;
        // 标记当前所迭代的位置。
        int position;
        internal IterationSampleIterator(IterationSample parent)
        {
            this.parent = parent;
            position = -1;
          }

        public bool MoveNext()
        {
            if (position != parent.values.Length)
            {
                position++;
            }
            return position < parent.values.Length;
        }

        public object Current
        {
            get
            {
                if (position == -1 ||
                    position == parent.values.Length)
                {
                    throw new InvalidOperationException();
                }
                int index = (position + parent.startingPoint);
                index = index % parent.values.Length;
                return parent.values[index];
            }
        }

        public void Reset()
        {
            position = -1;
        }
    }
}

这样就能像下面这样进行迭代:

// sample 是 IterationSample 对象
var iterator = sample.GetEnumerator();
while(iterator.MoveNext())
{
  item = iterator.Current;
  .....
}
或者用
foreach(var item in sample) 
{
    .....
}

实现迭代器的代码容易理解,但是实现代码繁琐。


3.C#2举例实现:使用yield关键字简化

同样实现上述例子中完的功能:

    GetEnumerator函数重新实现,包含yield return 语句。

    IterationSampleIterator 枚举器类 删除不需要了。

class IterationSample : IEnumerable
{
    object[] values;
    int startingPoint;

    public IterationSample(object[] values, int startingPoint)
    {
        this.values = values;
        this.startingPoint = startingPoint;
    }

    public IEnumerator GetEnumerator()
    {
      for (int index = 0; index <values.Length; index++)
      {
        yield return values[(index + startingPoint) % values.Length];
      }
    }
}

yield关键字是一个语法糖,背后其实生成了一个新的类。

具体实现原理可以看下面这篇文章,详细易懂:https://www.cnblogs.com/blueberryzzz/p/8678700.html#undefined

更详细的可以看这个例子(来自这里):

原始的Test代码:

using System;  
using System.Collections;  
  
class Test  
{  
    static IEnumerator GetCounter()  
    {  
        for (int count = 0; count < 10; count++)  
        {  
            yield return count;  
        }  
    }  
}  

真正的Test类:C#编译器针对包含yield的函数生成了一个新的类 <GetCounter>d__0

internal class Test  
{  
    // Note how this doesn't execute any of our original code  
    private static IEnumerator GetCounter()  
    {  
        return new <GetCounter>d__0(0);  
    }  
  
    // Nested type automatically created by the compiler to implement the iterator  
    [CompilerGenerated]  
    private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable  
    {  
        // Fields: there'll always be a "state" and "current", but the "count"  
        // comes from the local variable in our iterator block.  
        private int <>1__state;  
        private object <>2__current;  
        public int <count>5__1;  
  
        [DebuggerHidden]  
        public <GetCounter>d__0(int <>1__state)  
        {  
            this.<>1__state = <>1__state;  
        }  
  
        // Almost all of the real work happens here  
        private bool MoveNext()  
        {  
            switch (this.<>1__state)  
            {  
                case 0:  
                    this.<>1__state = -1;  
                    this.<count>5__1 = 0;  
                    while (this.<count>5__1 < 10)        //这里针对循环处理  
                    {  
                        this.<>2__current = this.<count>5__1;  
                        this.<>1__state = 1;  
                        return true;  
                    Label_004B:  
                        this.<>1__state = -1;  
                        this.<count>5__1++;  
                    }  
                    break;  
  
                case 1:  
                    goto Label_004B;  
            }  
            return false;  
        }  
  
        [DebuggerHidden]  
        void IEnumerator.Reset()  
        {  
            throw new NotSupportedException();  
        }  
  
        void IDisposable.Dispose()  
        {  
        }  
  
        object IEnumerator<object>.Current  
        {  
            [DebuggerHidden]  
            get  
            {  
                return this.<>2__current;  
            }  
        }  
  
        object IEnumerator.Current  
        {  
            [DebuggerHidden]  
            get  
            {  
                return this.<>2__current;  
            }  
        }  
    }  
}  

可以看出,其实就是把 原始函数里的内容,都放入到了MoveNext中了。

类似的,也可以看下面这个例子(来自深入理解C#)

CreateEnumerable 并没有实现GetEnumerator,以及实现MoveNext 和 Current函数,但是我们却可以在Main函数中,进行调用,说明其实是编译器给你生成了一个实现了这些接口的类。

(如果是CreateEnumerable返回 IEnumerable,编译器实现的类应该是继承自IEnumerator 和 IEnumeratable

如果是CreateEnumerable返回 IEnumerator,编译器实现的类应该是只继承自IEnumerator 

class IteratorWorkflow
{
    static readonly string Padding = new string(' ', 30);
    // 也可以返回 IEnumerator<int>,相应的Main里面就不需要先获取 iterable 了。
    static IEnumerable<int> CreateEnumerable()
    {
        Console.WriteLine("{0}Start of CreateEnumerable()", Padding);
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine("{0}About to yield {1}", Padding, i);
            // !!!遇到yield return 代码就停止执行,再下一次调用MoveNext的时候 继续执行。
            yield return i;
            Console.WriteLine("{0}After yield", Padding);
        }

        Console.WriteLine("{0}Yielding final value", Padding);
        yield return -1;

        Console.WriteLine("{0}End of GetEnumerator()", Padding);
    }

    static void Main()
    {
        // 此处不会真正调用CreateEnumerable()函数
        IEnumerable<int> iterable = CreateEnumerable();
        IEnumerator<int> iterator = iterable.GetEnumerator();

        Console.WriteLine("Starting to iterate");
        // 也可以用foreach来替代。这里展示了foreach的类似过程。
        while (true)
        {
            Console.WriteLine("Calling MoveNext()...");
            //  此处开始真正调用CreateEnumerable()函数
            bool result = iterator.MoveNext();
            Console.WriteLine("... MoveNext result={0}", result);
            if (!result)
            {
                break;
            }
            Console.WriteLine("Fetching Current...");
            Console.WriteLine("... Current result={0}", iterator.Current);
        }
    }
}

自己运行一下上述代码,看输出信息就知道了 编译器自动生成的迭代器的 工作原理了。

关于yield:

1.可以在方法属性索引器中使用 yield 来实现迭代器。 使用yield,函数返回类型必须是 IEnumberable<T>、IEnumberable、IEnumberator<T>和IEnumberator中的一个。函数参数不能是 ref out

2. try 和 catch 语句里面不能出现yield return 
3.不能在匿名方法中用迭代器代码块,也就是yield。


4.yield break 相当于普通方法中的return

    class YieldBreak
    {
        static IEnumerable<int> CountWithTimeLimit(DateTime limit)
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    // 大于2s,迭代块就退出了
                    yield break;
                }
                yield return i;
            }
        }

        static void Main()
        {
            DateTime stop = DateTime.Now.AddSeconds(2);
            foreach (int i in CountWithTimeLimit(stop))
            {
                Console.WriteLine("Received {0}", i);
                Thread.Sleep(300);
            }
        }
    }

5.finally块 被 实现为 Dispose

应该是编译器实现的类 实现了 IDisposable接口  上述链接中的例子也是这样,所以退出迭代器块的时候,就会调用Dispose函数。

class YieldBreakAndTryFinally
{
    static IEnumerable<int> CountWithTimeLimit(DateTime limit)
    {
        try
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    yield break;
                }
                yield return i;
            }
        }
        finally
        {
            // 停止使用迭代器块会执行finally,即Dispose的调用会触发finally块的执行。
            Console.WriteLine("Stopping!");
        }
    }

    static void Main()
    {
        DateTime stop = DateTime.Now.AddSeconds(2);
        foreach (int i in CountWithTimeLimit(stop))
        {
            Console.WriteLine("Received {0}", i);
            if(i > 3)
            {
              return;
            }
            Thread.Sleep(300);
        }
    }
}

 


6.实际开发中实现迭代器的例子

1.timetable 是一个时刻表的类。

方法一:用for循环 来表示时刻表中的每一天。

for (DateTime day = timetable.StartDate; day <= timetable.EndDate; day = day.AddDays(1))
{}

 方法二:实现迭代器,用foreach来表示时刻表中的每一天。

foreach (var day in timetable.DateRange)
{

}
// 需要实现DateRange是一个可迭代的集合,timetable类中的一个属性。
public IEnumerable <DateTime> DateRange
{
  get
  {
      // 原始的代码被放到了这里。
      for (DateTime day = timetable.StartDate; day <= timetable.EndDate; day = day.AddDays(1))
      {
          yield return day;
      }
  }
}

2.迭代文件中的行

原始方法:每次读文件中的行都需要的写如下的类似代码,繁琐

// reader阅读器 需要释放 
using(TextReader reader = File.OpenText(filename))
{
  string line;
  while((line = reader.ReadLine())!= null)
  {
    //针对line进行某些处理。
  }
}

改进方法基础迭代器版本

foreach (string line in ReadLines("test.txt"))
{
  //针对line进行某些处理。
}

public static IEnumerable<string> ReadLines(string filename)
{
  // using 扮演了 try/finally 块的角色。具体实现了什么呢?
  // 在到达文件末尾或在中途调用 IEnumerabtor<string>.Dispose方法时,将进入finally块。
  // 同样,是把原始的代码移动到这里
  using (TextReader reader = File.OpenText(filename))
  {
      string line;
      while ((line = reader.ReadLine()) != null)
      {
          yield return line;
      }
  }
}       

改进迭代器版本:

如果不是打开文本获取阅读器而是通过网络流读取文本,或者不是用utf8编码格式,应该怎么扩展

(通过网络流读取文本 是怎么样的?)
所以直接传入 TextReader参数 不好,这里传入一个 Func<TextReader> 类型的参数。

public static IEnumerable<string> ReadLines(Func<TextReader> provider)
{
  //同样调用了 using
  using (TextReader reader = provider())
  {
      string line;
      while ((line = reader.ReadLine()) != null)
      {
          yield return line;
      }
  }
}
// 调用上面
public static IEnumerable<string> ReadLines(string filename, Encoding encoding)
{
  // 复习:使用了匿名方法,捕获所在方法的参数。
  return ReadLines(delegate
  {
    return File.OpenText(filename, encoding);
  });
}
// 调用上面,默认使用utf8
public static IEnumerable<string> ReadLines(string filename)
{
  return ReadLines(filename, Encoding.UTF8);
}

参考:

《深入理解C#》

https://www.cnblogs.com/blueberryzzz/p/8678700.html#undefined

猜你喜欢

转载自blog.csdn.net/u012138730/article/details/81148086