• C#迭代器


    在.NET中,迭代器模式是通过IEnumerator和IEnumerable接口以及它们的泛型版本来实现的。如果某个类实现了IEnumerable接口,就说明它可以被迭代访问,调用GetEnumerator()方法将返回IEnumerator的实现,这个就是迭代器本身。

    在C# 1.0中,利用foreach语句实现了访问迭代器的内置支持,让集合的遍历变得简单、明了。其实,foreach的实现就是调用GetEnumerator和MoveNext方法以及Current属性。所以说,在C# 1.0中要获得迭代器就必须实现IEnumerable接口中的GetEnumerator方法,要实现一个迭代器就要实现IEnumerator接口中的MoveNext和Reset方法

    在C# 2.0中提供的语法糖来简化迭代器的实现,可以通过yield关键字来简化迭代器的实现。

    C# 1.0中的迭代器实现

    假设我们要实现一个字符列表类型,并且可以通过foreach来遍历这个类型。那么,在C# 1.0中,就要实现IEnumerable和IEnumerator接口。

    namespace IteratorTest
    {
        class Program
        {
            static void Main(string[] args)
            {
                CharList charList = new CharList("Hello World");
                foreach (var c in charList)
                {
                    Console.WriteLine(c);
                }
    
                Console.Read();
            }
        }
    
        class CharList : IEnumerable
        {
            public string TargetStr { get; set; }
    
            public CharList(string str)
            {
                this.TargetStr = str;
            }
    
            public IEnumerator GetEnumerator()
            {
                return new CharIterator(this.TargetStr);
            }
        }
    
        class CharIterator : IEnumerator
        {
            //引用要遍历的字符串
            public string TargetStr { get; set; }
            //指出当前遍历的位置
            public int position { get; set; }
    
            public CharIterator(string targetStr)
            {
                this.TargetStr = targetStr;
                this.position = this.TargetStr.Length;
            }
    
            public object Current
            {
                get
                {
                    if (this.position == -1 || this.position == this.TargetStr.Length)
                    {
                        throw new InvalidOperationException();
                    }
                    return this.TargetStr[this.position];
                }
            }
    
            public bool MoveNext()
            {
                //如果满足继续遍历的条件,设置position的值
                if (this.position != -1)
                {
                    this.position--;
                }
                return this.position > -1;
            }
    
            public void Reset()
            {
                this.position = this.TargetStr.Length;
            }
        }
    }

    在上面的例子中,CharIterator就是迭代器的实现,position字段存储当前的迭代位置,通过Current属性可以得到当前迭代位置的元素,MoveNext方法用于更新迭代位置,并且查看下一个迭代位置是不是有效的。

    当我们通过VS单步调试下面语句的时候

    foreach (var c in charList)

    代码首先执行到foreach语句的charList处获得迭代器CharIterator的实例,然后代码执行到in会调用迭代器的MoveNext方法,最后变量c会得到迭代器Current属性的值;前面的步骤结束后,会开始一轮新的循环,调用MoveNext方法,获取Current属性的值。

    C# 2.0通过yield简化迭代器实现

    通过C# 1.0中迭代器的代码看到,要实现一个迭代器就要实现IEnumerator接口,然后实现IEnumerator接口中的MoveNext、Reset方法和Current属性。

    在C# 2.0中可以直接使用yield语句来简化迭代器的实现。

    class CharList : IEnumerable
    {
        public string TargetStr { get; set; }
    
        public CharList(string str)
        {
            this.TargetStr = str;
        }
    
        public IEnumerator GetEnumerator()
        {
            for (int index = this.TargetStr.Length; index > 0; index--)
            {
                yield return this.TargetStr[index-1];
            }
        }
    }

    通过上面的代码可以看到,通过使用yield return语句,我们可以替换掉整个CharIterator类。

    yield return语句就是告诉编译器,要实现一个迭代器块。如果GetEnumerator方法的返回类型是非泛型接口,那么迭代器块的生成类型(yield type)是object,否则就是泛型接口的类型参数。

    通过IL代码可以看到,对于yield return语句语句,编译器为我们生成了一个嵌套的类型(nested type) <GetEnumerator>d__0,并且这个类实现了IEnumerator接口。

    当编译器遇到迭代块时,它创建了一个实现了状态机的内部类。这个类记住了我们迭代器的准确当前位置以及本地变量,包括参数。这个类有点类似与C# 1.0中手写的那段代码,它将所有需要记录的状态保存为实例变量。为了实现一个迭代器,这个状态机需要按顺序执行的操作:

    • 它必须具有某个初始状态
    • 当MoveNext被调用时,他需要执行GetEnumerator方法中的代码来准备下一个待返回的数据
    • 当调用Current属性是,它必须返回上一个生成的数据
    • 需要知道什么时候迭代结束,MoveNext会返回false

    注意,当我们想要避免迭代器中的装箱和拆箱是,就要实现迭代器的泛型版本,由于泛型IEnumerable <T>接口继承了泛型型IEnumerable接口,我们需要在泛型迭代器代码中加入

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }

    这样,非泛型方法转而调用泛型方法,从而不需要再去实现非泛型的IEnumerable接口了。

    迭代器的工作流程

    前面简单提到了迭代器的工作流程,下面我们通过一个例子进一步看看迭代器工作流程。

    class Program
    {
        static readonly String Padding = new String(' ', 30);
        static IEnumerable<Int32> 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 i;
                Console.WriteLine("{0}After yield", Padding);
            }
            Console.WriteLine("{0}Yielding final value", Padding);
            yield return -1;
            Console.WriteLine("{0}End of CreateEnumerable()", Padding);
        }
    
        static void Main(string[] args)
        {
            IEnumerable<Int32> iterable = CreateEnumerable();
            IEnumerator<Int32> iterator = iterable.GetEnumerator();
            Console.WriteLine("Starting to iterate");
            while (true)
            {
                Console.WriteLine("Calling MoveNext()...");
                Boolean result = iterator.MoveNext();
                Console.WriteLine("...MoveNext result={0}", result);
                if (!result)
                {
                    break;
                }
                Console.WriteLine("Fetching Current...");
                Console.WriteLine("...Current result={0}", iterator.Current);
            }
            Console.Read();
        }
    }

    一般迭代器都会结合foreach语句,然后foreach会在最后调用Dispose方法。这里为了演示,代码中使用while语句实现循环。

    稍微打断一下,插入一个内容的介绍,通常为了实现IEnumerable,我们只会返回IEnumerator;如果仅仅是在方法中生成一个序列,可以返回IEnumerable。所以将代码改为下面的方式也可以工作:

    static IEnumerator<Int32> CreateEnumerable()
    {
        ……
    }
    
    ……
    //IEnumerable<Int32> iterable = CreateEnumerable();
    IEnumerator<Int32> iterator = CreateEnumerable();

    两种方式的IL代码是不同的,这里只列出了编译器内嵌类型实现了那些接口,更详细的内容可以通过ILSpy查看:

    返回IEnumerable

    .class nested private auto ansi sealed beforefieldinit '<CreateEnumerable>d__0'
        extends [mscorlib]System.Object
        implements class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>,
                   [mscorlib]System.Collections.IEnumerable,
                   class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>,
                   [mscorlib]System.Collections.IEnumerator,
                   [mscorlib]System.IDisposable
    {……}

    返回IEnumerator

    .class nested private auto ansi sealed beforefieldinit '<CreateEnumerable>d__0'
        extends [mscorlib]System.Object
        implements class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>,
                   [mscorlib]System.Collections.IEnumerator,
                   [mscorlib]System.IDisposable
    {……}

    回到这个例子,程序的输出结果为:

    在这段代码中有几个注意点:

    • 直到第一次调用MoveNext,CreateEnumerable中的方法才被调用
    • 在调用MoveNext的时候,已经做好了所有操作,获取Current属性并没有执行任何代码
    • 代码在yield return之后就停止执行,在下一次调用MoveNext方法的时候继续执行
    • 同一个方法的不同地方可以有多个yield return语句
    • 代码不会在最后的yield return处结束,而是通过返回false的MoveNext调用来结束方法的执行

    第一点尤为重要:这意味着如果在方法调用时需要立即执行,就不能使用迭代器块。例如如果将参数验证放在迭代块中,那么他将不能够很好的起作用,这是经常会导致的错误的地方,而且这种错误不容易发现。

    进一步了解迭代器工作流程

    在常规方法中,return语句通常有两种作用:一是返回调用者执行的结果。二是终止方法的执行,在终止之前执行finally语句中的方法。在上面的例子中,我们看到了yield return语句只是短暂的退出了方法,在调用MoveNext的时候继续执行。根本没有检查finally代码块的行为。

    如何真正的退出方法?退出方法时finnally语句块如何执行?下面来看看一个比较简单的结构:yield break语句块。

    使用yield return结束迭代器执行

    通常方法只有一个退出点,不过有时候我们想"提早退出。对于迭代器块,使用yield break就能达到想要的效果。它能够马上终止迭代,使得下一次调用MoveNext的时候返回false。

    下面的代码演示了从1迭代到100,但是时间超时的时候就停止了迭代:

    class Program
    {
        static IEnumerable<Int32> CountWithTimeLimit(DateTime limit)
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    yield break;
                }
                yield return i;
            }         
        }
    
        static void Main(string[] args)
        {
            DateTime stop = DateTime.Now.AddSeconds(2);
            foreach (Int32 i in CountWithTimeLimit(stop))
            {
                Console.WriteLine("Received {0}", i);
                Thread.Sleep(300);
            }
            Console.WriteLine("End of Main");
            Console.Read();
        }
    }

    从程序的输出可以看到,yield break语句的行为类似于普通方法的return,迭代器的迭代被停止,并提前退出。

    下面就看看finally语句块是如何执行以及何时执行的。

    finally代码块的执行

    通常,finally语句块在当方法执行退出特定区域时就会执行。迭代块中的finally语句和普通方法中的finally语句块不一样。就像我们看到的,yield return语句停止了方法的执行,而不是退出方法,根据这一逻辑,在这种情况下,finally语句块中的语句不会执行。

    但当碰到yield break语句的时候,就会执行finally 语句块。一般在迭代块中使用finally语句来释放资源,就像使用using语句一样。

    下面修改前面的例子来看finally语句如何执行。不管是迭代到了100次或者是由于时间到了停止了迭代,或者是抛出了异常,finally语句总会执行。

    static IEnumerable<Int32> CountWithTimeLimit(DateTime limit)
    {
        try
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    yield break;
                }
                yield return i;
            }
        }
        finally
        {
            Console.WriteLine("Stopping");
        }
    }

    只有在调用MoveNext后迭代块中的语句才会执行,那么如果不掉用MoveNext呢,如果调用几次MoveNext然后停止调用,结果会怎么样呢?看下面一段代码

    DateTime stop = DateTime.Now.AddSeconds(2);
    foreach (Int32 i in CountWithTimeLimit(stop))
    {
        if (i > 3)
        {
            Console.WriteLine("Returning");
            return;
        }
        Thread.Sleep(300);
    }

    在上面代码中,我们不是提前停止执行迭代器块,而是提前停止使用迭代器。在foreach循环中的return语句执行后,迭代器的finally代码也被执行了。

    之所以这里的finally被执行了,是因为foreach会在自己的finall代码块中调用IEnumerator 所提供的Dispose方法。当迭代器完成迭代之前,如果调用由迭代器代码块创建的迭代器上的Dispose方法,那么状态机就会执行在代码当前"暂停"位置范围内的任何finally代码块。这有点复杂,但是结果很容易解释:只要使用foreach循环,迭代块中的finally块会如期望的那样执行。

    上面的描述可以通过下面的代码进行验证:

    static void Main(string[] args)
    {
        DateTime stop = DateTime.Now.AddSeconds(2);
        IEnumerable<Int32> iterable = CountWithTimeLimit(stop);
        IEnumerator<Int32> iterator = iterable.GetEnumerator();
    
        iterator.MoveNext();
        Console.WriteLine("Reveived {0}", iterator.Current);
    
        iterator.MoveNext();
        Console.WriteLine("Reveived {0}", iterator.Current);
    
        //显示调用Dispose来执行迭代器的finally代码块
        //iterator.Dispose();
    
        Console.WriteLine("End of Main");
        Console.Read();
    }

    总结

    本文中看到了C# 1.0中如何实现一个迭代器,以及通过C# 2.0中提供的yield return如何简化一个迭代器的实现。通过对迭代器工作流程的介绍,看到了yield return的延迟执行,yield return语句只表示"暂时的"退出方法。

  • 相关阅读:
    剑指offer 21:包含min函数的栈
    导航页的开发--手机web app开发笔记(四)
    manifest.json 解析--手机web app开发笔记(三-2)
    manifest.json 解析--手机web app开发笔记(三-1)
    默认文档解析--手机web app开发笔记(二)
    手机web app开发笔记
    JS开发备忘笔记-- Javascript中document.execCommand()的用法
    JS时间格式转换
    HBuilderx 模拟器调试设置
    截图小工具开发笔记
  • 原文地址:https://www.cnblogs.com/wilber2013/p/4299529.html
Copyright © 2020-2023  润新知