C# in depth (第六章 实现迭代器的捷径)

  • 迭代器模式是行为模式的一种范例,行为模式是一种简化对象之间通信的设计模式。这是一种非常易于理解和使用的模式。实际上,它允许你访问一个数据项序列中的所有元素,而无须关心序列是什么类型----数组,列表,链表或任何其他类型。它能非常有效地构建出一个数据管道,经过一系列不同的转换或过滤后再从管道的另一端出来。实际上,这也是LINQ的核心模式之一。
  • 可以将迭代器想象成数据库的游标,即序列中的某个位置。迭代器只能在序列中向前移动,而且对于同一个序列可能同时存在多个迭代器操作。

6.1 C#1.0:手写迭代器的痛苦

    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;    //1.正在迭代的集合
            int position;              //2.指出遍历到的位置

            internal IterationSampleIterator(IterationSample parent)
            {
                this.parent = parent;
                position = -1;        //3.在第一个元素之前开始
            }

            public bool MoveNext()
            {
                if (position != parent.values.Length)//4.如果仍要遍历,那么增加position的值
                {
                    position++;
                }
                return position < parent.values.Length;
            }

            public object Current
            {
                get
                {
                    if (position == -1 ||
                        position == parent.values.Length)//5.防止访问第一个元素之前和最后一个元素之后
                    {
                        throw new InvalidOperationException();
                    }
                    int index = (position + parent.startingPoint); //6.实现封装
                    index = index % parent.values.Length;
                    return parent.values[index];
                }
            }

            public void Reset()
            {
                position = -1;              //7.返回第一个元素之前
            }
        }

        static void Main()
        {
            object[] values = { "a", "b", "c", "d", "e" };
            IterationSample collection = new IterationSample(values, 3);
            foreach (object x in collection)
            {
                Console.WriteLine(x);
            }
        }
    }
  •  首先要知道,我们需要在某个地方存储某个状态。迭代器模式的一个重要方面就是,不用一次返回所有数据----调用代码一次只需获取一个元素。这意味着我们需要确定访问到了数组中的哪个位置。在了解C#2.0编译器为我们所做的事情时,迭代器的这种状态特制十分重要,因此,要密切关注本例中的状态。
  1. 我们要记住迭代的原始值的集合 1
  2. 并用简单的从零开始的数组跟踪我们所在的位置 2
  3. 为了返回元素,要根据原始点对索引进行偏移。6
  4. 要让迭代器逻辑上从第一个元素的位置之前开始 3
  5. 所以在第一次使用Current属性之前,调用代码必须调用MoveNext方法。4 中的条件增量可以保证 5的条件判断简单准确,即使在程序第一次报告无可用数据后又调用MoveNext也没有问题。
  6. 为了重置迭代器,我们将我们的逻辑位置设置回"第一个元素之前“ 7

6.2.1  迭代器和yield return 简介

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

        public IteratorBlockIterationSample(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]; //完全替换掉了整个IterationSampleIterat
            }
        }

        static void Main()
        {
            object[] values = { "a", "b", "c", "d", "e" };
            IteratorBlockIterationSample collection = new IteratorBlockIterationSample(values, 3);
            foreach (object x in collection)
            {
                Console.WriteLine(x);
            }
        }
    }
  • 这个方法不是一个普通的方法,而是实现一个迭代器块的方法。这个方法被声明为返回一个IEnumerator接口。
  • 当编译器看到迭代器块时,会为状态机创建一个嵌套类型,来正确记录块中的位置以及局部变量(包括参数)的值。所创建的类类似于我们之前用普通方法实现的类,用实例变量来保存所有必要的状态。下面来看一下,要实现迭代器,这个状态机要做哪些事情
  1. 它必须具有某个初始状态:
  2. 每次调用MoveNext时,在提供下一个值之前(换句话说,就是执行到yield return 语句之前),它需要执行GetEnumerator方法中的代码
  3. 使用Current属性时,它必须返回我们生成的上一个值。
  4. 它必须知道何时完成生成值的操作,以便MoveNext返回False。

6.2.2 观察迭代器的工作流程

class IteratorWorkflow
    {
        static readonly string Padding = new string(' ', 30);

        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 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()
        {
            IEnumerable<int> iterable = CreateEnumerable();
            IEnumerator<int> iterator = iterable.GetEnumerator();


            Console.WriteLine("Starting to iterate");
            while (true)
            {
                Console.WriteLine("Calling MoveNext()...");
                bool result = iterator.MoveNext();
                Console.WriteLine("... MoveNext result={0}", result);
                if (!result)
                {
                    break;
                }
                Console.WriteLine("Fetching Current...");
                Console.WriteLine("... Current result={0}", iterator.Current);
            }
        }
    }
  • 在第一次调用MoveNext之前,CreateEnumerable中的代码不会被调用;
  • 所有工作在调用MoveNext时就完成了,获取Current的值不会执行任何代码;
  • 在yield return的位置, 代码就停止执行,在下一次调用MoveNext时又继续执行;
  • 在一个方法中的不同地方可以编写多个yield return语句;
  • 代码不会在最后的yield return处结束,而是通过返回false的MoveNext调用结束来结束方法的执行。

6.2.3 进一步了解迭代器执行流程

  1. 使用yield break结束迭代器的执行  (如果你希望”提早退出“,那么yield break 正是你所需要的。它实际上终止了迭代器的运行,让当前对MoveNex的调用返回false。

 

class YieldBreak
    {
        static IEnumerable<int> CountWithTimeLimit(DateTime limit)
        {
            for (int i = 1; i <= 100; i++)
            {
                if (DateTime.Now >= limit)
                {
                    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);
            }
        }
    }

       2. finally 代码块的执行

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

        static void Main()
        {
            DateTime stop = DateTime.Now.AddSeconds(2);
            foreach (int i in CountWithTimeLimit(stop))
            {
                Console.WriteLine("Received {0}", i);
                Thread.Sleep(300);
            }
        }
  • finally会在yield break之后执行
 static IEnumerable<int> CountWithTimeLimit(DateTime limit)
        {
            try
            {
                for (int i = 1; i <= 100; i++)
                {
                    if (DateTime.Now >= limit)
                    {
                        yield break;
                    }
                    yield return i;
                }
            }
            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)
                {
                    Console.WriteLine("Returning");
                    return;
                }
                Thread.Sleep(300);
            }
        }
  • finally会在Main函数里的return之后执行;只要调用者使用了foreach循环,迭代器块中的finally将按照你期望的方式工作。

6.2.4具体实现中的奇特之处

6.3 真实的迭代器示例

6.3.1迭代时刻表中的日期

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

}
  public IEnumerable<DateTime> DateRange
{
      get
     {
            for(DateTime day = StartDate;
                day<=EndDate;
                day =day.AddDays(1))
           {
               yield return day;
           }
      }
}

 6.3.2 迭代文件中的行

class LineReader
    {
        public static IEnumerable<string> ReadLines(string filename)
        {
            using (TextReader reader = File.OpenText(filename))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    yield return line;
                }
            }
        }

        static void Main()
        {
            foreach (string line in ReadLines("../../LineReader.cs"))
            {
                Console.WriteLine(line);
            }
        }
    }

 6.3.3 使用迭代器块和谓词对项进行延迟过滤

 class FakeLinq
    {
        public static IEnumerable<T> Where<T>(IEnumerable<T> source, Predicate<T> predicate)
        {
            if (source == null || predicate == null)//1.热情的检查参数
            {
                throw new ArgumentNullException();
            }
            return WhereImpl(source, predicate);  //2.懒惰地(延时)处理数据
        }

        private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Predicate<T> predicate)
        {
            foreach (T item in source)
            {
                if (predicate(item)) //3.检查当前项与谓词是否匹配
                {
                    yield return item;
                }
            }
        }

        static void Main()
        {
            IEnumerable<string> lines = LineReader.ReadLines("../../FakeLinq.cs");
            Predicate<string> predicate = delegate(string line)
                { return line.StartsWith("using"); };
            foreach (string line in Where(lines, predicate))
            {
                Console.WriteLine(line);
            }
        }
    }

       假设将所有的内容都放入同一个方法中,那么调用Where<string>(null,null)时会发生什么呢? 答案是什么也不发生,或至少不会抛出我们认为的异常。这是由迭代器块的延迟语义所决定的。因为在第一次调用MoveNext()之前,不会执行方法体内的任何代码。所以是像代码清单6-9中那样将方法一分为二。

  • 首先在普通方法中检查参数 1.
  • 然后调用使用迭代器块实现的方法,在得到请求时再延时处理数据2.
  • 迭代器块本身十分简单:对于原始集合中的每一项,我们测试谓词3.
posted @ 2015-08-14 16:56  莱茵哈特  阅读(199)  评论(0编辑  收藏  举报