详解C# 迭代器

    迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通信的模式,也是一种很是容易理解和使用的模式。简单来讲,迭代器模式使得你可以获取到序列中的全部元素而不用关心是其类型是array,list,linked list或者是其余什么序列结构。这一点使得可以很是高效的构建数据处理通道(data pipeline)--即数据可以进入处理通道,进行一系列的变换,或者过滤,而后获得结果。事实上,这正是LINQ的核心模式。数据库

    在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。若是一个类实现了IEnumerable接口,那么就可以被迭代;调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器自己。迭代器相似数据库中的游标,他是数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中能够有多个迭代器同时对数据进行操做。设计模式

    在C#1中已经内建了对迭代器的支持,那就是foreach语句。使得可以进行比for循环语句更直接和简单的对集合的迭代,编译器会将foreach编译来调用GetEnumerator和MoveNext方法以及Current属性,若是对象实现了IDisposable接口,在迭代完成以后会释放迭代器。可是在C#1中,实现一个迭代器是相对来讲有点繁琐的操做。C#2使得这一工做变得大为简单,节省了实现迭代器的很多工做。数组

接下来,咱们来看如何实现一个迭代器以及C#2对于迭代器实现的简化,而后再列举几个迭代器在现实生活中的例子。网络

 

1. C#1:手动实现迭代器的繁琐

 

    假设咱们须要实现一个基于环形缓冲的新的集合类型。咱们将实现IEnumerable接口,使得用户可以很容易的利用该集合中的全部元素。咱们的忽略其余细节,将注意力仅仅集中在如何实现迭代器上。集合将值存储在数组中,集合可以设置迭代的起始点,例如,假设集合有5个元素,你可以将起始点设为2,那么迭代输出为2,3,4,0,最后是1.闭包

    为了可以简单展现,咱们提供了一个设置值和起始点的构造函数。使得咱们可以如下面这种方式遍历集合:ide

object[] values = { "a", "b", "c", "d", "e" };
IterationSample collection = new IterationSample(values, 3);
foreach (object x in collection)
{
    Console.WriteLine(x);
}

因为咱们将起始点设置为3,因此集合输出的结果是d,e,a,b及c,如今,咱们来看如何实现 IterationSample 类的迭代器:函数

class IterationSample : IEnumerable
{
    Object[] values;
    Int32 startingPoint;
    public IterationSample(Object[] values, Int32 startingPoint)
    {
        this.values = values;
        this.startingPoint = startingPoint;
    }
    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

    咱们尚未实现GetEnumerator方法,可是如何写GetEnumerator部分的逻辑呢,第一就是要将游标的当前状态存在某一个地方。一方面是迭代器模式并非一次返回全部的数据,而是客户端一次只请求一个数据。这就意味着咱们要记录客户当前请求到了集合中的那一个记录。C#2编译器对于迭代器的状态保存为咱们作了不少工做。this

       如今来看看,要保存哪些状态以及状态存在哪一个地方,设想咱们试图将状态保存在IterationSample集合中,使得它实现IEnumerator和IEnumerable方法。咋一看,看起来可能,毕竟数据在正确的地方,包括起始位置。咱们的GetEnumerator方法仅仅返回this。可是这种方法有一个很重要的问题,若是GetEnumerator方法调用屡次,那么多个独立的迭代器就会返回。例如,咱们可使用两个嵌套的foreach语句,来获取全部可能的值对。这两个迭代须要彼此独立。这意味着咱们须要每次调用GetEnumerator时返回的两个迭代器对象必须保持独立。咱们仍旧能够直接在IterationSample类中经过相应函数实现。可是咱们的类拥有了多个职责,这位背了单一职责原则。编码

     所以,咱们来建立另一个类来实现迭代器自己。咱们使用C#中的内部类来实现这一逻辑。代码以下:spa

class IterationSampleEnumerator : IEnumerator
{
    IterationSample parent;//迭代的对象  #1
    Int32 position;//当前游标的位置 #2
    internal IterationSampleEnumerator(IterationSample parent)
    {
        this.parent = parent;
        position = -1;// 数组元素下标从0开始,初始时默认当前游标设置为 -1,即在第一个元素以前, #3
    }

    public bool MoveNext()
    {
        if (position != parent.values.Length) //判断当前位置是否为最后一个,若是不是游标自增 #4
        {
            position++;
        }
        return position < parent.values.Length;
    }

    public object Current
    {
        get
        {
            if (position == -1 || position == parent.values.Length)//第一个以前和最后一个自后的访问非法 #5
            {
                throw new InvalidOperationException();
            }
            Int32 index = position + parent.startingPoint;//考虑自定义开始位置的状况  #6
            index = index % parent.values.Length;
            return parent.values[index];
        }
    }

    public void Reset()
    {
        position = -1;//将游标重置为-1  #7
    }
}

要实现一个简单的迭代器须要手动写这么多的代码:须要记录迭代的原始集合#1,记录当前游标位置#2,返回元素时,根据当前游标和数组定义的起始位置设置定迭代器在数组中的位置#6。初始化时,将当前位置设定在第一个元素以前#3,当第一次调用迭代器时首先须要调用MoveNext,而后再调用Current属性。在游标自增时对当前位置进行条件判断#4,使得即便当第一次调用MoveNext时没有可返回的元素也不至于出错#5。重置迭代器时,咱们将当前游标的位置还原到第一个元素以前#7。

    除告终合当前游标位置和自定义的起始位置返回正确的值这点容易出错外,上面的代码很是直观。如今,只须要在IterationSample类的GetEnumerator方法中返回咱们当才编写的迭代类便可:

public IEnumerator GetEnumerator()
{
    return new IterationSampleEnumerator(this);
}

    值得注意的是,上面只是一个相对简单的例子,没有太多的状态须要跟踪,不用检查集合在迭代的过程当中是否发生了变化。为了实现一个简单的迭代器,在C#1中咱们实现了如此多的代码。在使用Framework自带的实现了IEnumerable接口的集合时咱们使用foreach很方便,可是当咱们书写本身的集合来实现迭代时须要编写这么多的代码。

    在C#1中,大概须要40行代码来实现一个简单的迭代器,如今看看C#2对这一过程的改进。

 

2. C#2:经过yield语句简化迭代

 

2.1 引入迭代块(iterator)和yield return 语句

C#2使得迭代变得更加简单--减小了不少代码量也使得代码更加的优雅。下面的代码展现了再C#2中实现GetEnumerator方法的完整代码:

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

简单几行代码就可以彻底实现IterationSampleIterator类所须要的功能。方法看起来很普通,除了使用了yield return。这条语句告诉编译器这不是一个普通的方法,而是一个须要执行的迭代块(yield block),他返回一个IEnumerator对象,你可以使用迭代块来执行迭代方法并返回一个IEnumerable须要实现的类型,IEnumerator或者对应的泛型。若是实现的是非泛型版本的接口,迭代块返的yield type是Object类型,不然返回的是相应的泛型类型。例如,若是方法实现IEnumerable<String>接口,那么yield返回的类型就是String类型。 在迭代块中除了yield return外,不容许出现普通的return语句。块中的全部yield return 语句必须返回和块的最后返回类型兼容的类型。举个例子,若是方法定义须要返回IEnumeratble<String>类型的话,不能yield return 1 。 须要强调的一点是,对于迭代块,虽然咱们写的方法看起来像是在顺序执行,实际上咱们是让编译器来为咱们建立了一个状态机。这就是在C#1中咱们书写的那部分代码---调用者每次调用只须要返回一个值,所以咱们须要记住最后一次返回值时,在集合中位置。 当编译器遇到迭代块是,它建立了一个实现了状态机的内部类。这个类记住了咱们迭代器的准确当前位置以及本地变量,包括参数。这个类有点相似与咱们以前手写的那段代码,他将全部须要记录的状态保存为实例变量。下面来看看,为了实现一个迭代器,这个状态机须要按顺序执行的操做:

  • 它须要一些初始的状态
  • 当MoveNext被调用时,他须要执行GetEnumerator方法中的代码来准备下一个待返回的数据。
  • 当调用Current属性是,须要返回yielded的值。
  • 须要知道何时迭代结束是,MoveNext会返回false

下面来看看迭代器的执行顺序。

 

2.2 迭代器的执行流程

以下的代码,展现了迭代器的执行流程,代码输出(0,1,2,-1)而后终止。

class Program
{
    static readonly String Padding = new String(' ', 30);
    static IEnumerable<Int32> CreateEnumerable()
    {
        Console.WriteLine("{0} CreateEnumerable()方法开始", Padding);
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine("{0}开始 yield {1}", i);
            yield return i;
            Console.WriteLine("{0}yield 结束", Padding);
        }
        Console.WriteLine("{0} Yielding最后一个值", Padding);
        yield return -1;
        Console.WriteLine("{0} CreateEnumerable()方法结束", Padding);
    }

    static void Main(string[] args)
    {
        IEnumerable<Int32> iterable = CreateEnumerable();
        IEnumerator<Int32> iterator = iterable.GetEnumerator();
        Console.WriteLine("开始迭代");
        while (true)
        {
            Console.WriteLine("调用MoveNext方法……");
            Boolean result = iterator.MoveNext();
            Console.WriteLine("MoveNext方法返回的{0}", result);
            if (!result)
            {
                break;
            }
            Console.WriteLine("获取当前值……");
            Console.WriteLine("获取到的当前值为{0}", iterator.Current);
        }
        Console.ReadKey();
    }
}

为了展现迭代的细节,以上代码使用了while循环,正常状况下通常使用foreach。和上次不一样,此次在迭代方法中咱们返回的是IEnumerable;对象而不是IEnumerator;对象。一般,为了实现IEnumerable接口,只须要返回IEnumerator对象便可;若是自是想从一个方法中返回一些列的数据,那么使用IEnumerable.如下是输出结果:

 

从输出结果中能够看出一下几点:

  • 直到第一次调用MoveNext,CreateEnumerable中的方法才被调用。
  • 在调用MoveNext的时候,已经作好了全部操做,返回Current属性并无执行任何代码。
  • 代码在yield return以后就中止执行,等待下一次调用MoveNext方法的时候继续执行。
  • 在方法中能够有多个yield return语句。
  • 在最后一个yield return执行完成后,代码并无终止。调用MoveNext返回false使得方法结束。

    第一点尤其重要:这意味着,不能在迭代块中写任何在方法调用时须要当即执行的代码--好比说参数验证。若是将参数验证放在迭代块中,那么他将不可以很好的起做用,这是常常会致使的错误的地方,并且这种错误不容易发现。

    下面来看如何中止迭代,以及finally语句块的特殊执行方式。

 

2.3 迭代器的特殊执行流程

    在普通的方法中,return语句一般有两种做用,一是返回调用者执行的结果。二是终止方法的执行,在终止以前执行finally语句中的方法。在上面的例子中,咱们看到了yield return语句只是短暂的退出了方法,在MoveNext再次调用的时候继续执行。在这里咱们没有写finally语句块。如何真正的退出方法,退出方法时finnally语句块如何执行,下面来看看一个比较简单的结构:yield break语句块。

使用 yield break 结束一个迭代

    一般咱们要作的是使方法只有一个退出点,一般,多个退出点的程序会使得代码不易阅读,特别是使用try catch finally等语句块进行资源清理以及异常处理的时候。在使用迭代块的时候也会遇到这样的问题,但若是你想早点退出迭代,那么使用yield break就能达到想要的效果。他可以立刻终止迭代,使得下一次调用MoveNext的时候返回false。

下面的代码演示了从1迭代到100,可是时间超时的时候就中止了迭代。

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("中止迭代!"); Console.ReadKey();
    }
}
static void Main(string[] args)
{
    DateTime stop = DateTime.Now.AddSeconds(2);
    foreach (Int32 i in CountWithTimeLimit(stop))
    {
        Console.WriteLine("返回 {0}", i);
        Thread.Sleep(300);
    }
}

下图是输出结果,能够看出迭代语句正常终止,yield return语句和普通方法中的return语句同样,下面来看看finally语句块是何时以及如何执行的。

 

Finally语句块的执行

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

    但当碰到yield break语句的时候,就会执行finally 语句块,这根普通方法中的return同样。通常在迭代块中使用finally语句来释放资源,就像使用using语句同样。

    下面来看finally语句如何执行。

   无论是迭代到了100次或者是因为时间到了中止了迭代,或者是抛出了异常,finally语句总会执行,可是在有些状况下,咱们不想让finally语句块被执行。

    只有在调用MoveNext后迭代块中的语句才会执行,那么若是不掉用MoveNext呢,若是调用几回MoveNext而后中止调用,结果会怎么样呢?请看下面的代码?

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

   在forech中,return语句以后,由于CountWithTimeLimit中有finally块因此代码继续执行CountWithTimeLimit中的finally语句块。foreach语句会调用GetEnumerator返回的迭代器的Dispose方法。在结束迭代以前调用包含迭代块的迭代器的Dispose方法时,状态机会执行在迭代器范围内处于暂停状态下的代码范围内的全部finally块,这有点复杂,可是结果很容易解释:只有使用foreach调用迭代,迭代块中的finally块会如指望的那样执行。下面能够用代码验证以上结论:

IEnumerable<Int32> iterable = CountWithTimeLimit(stop);
IEnumerator<Int32> iterator = iterable.GetEnumerator();

iterator.MoveNext();
Console.WriteLine("返回 {0}", iterator.Current);

iterator.MoveNext();
Console.WriteLine("返回 {0}", iterator.Current);
Console.ReadKey();

代码输出以下:

上图能够看出,中止迭代没有打印出来,当咱们手动调用iterator的Dispose方法时,会看到以下的结果。在迭代器迭代结束前终止迭代器的状况不多见,也不多不使用foreach语句而是手动来实现迭代,若是要手动实现迭代,别忘了在迭代器外面使用using语句,以确保可以执行迭代器的Dispose方法进而执行finally语句块。 

下面来看看微软对迭代器的一些实现中的特殊行为:

 

2.4 迭代器执行中的特殊行为

 

    若是使用C#2的编译器将迭代块编译,而后使用ildsam或者Reflector查看生成的IL代码,你会发如今幕后编译器回味咱们生成了一些嵌套的类型(nested type).下图是使用Ildsam来查看生成的IL ,最下面两行是代码中的的两个静态方法,上面蓝色的<CountWithTimeLimit>d_0是编译器为咱们生成的类(尖括号只是类名,和泛型无关),代码中能够看出该类实现了那些接口,以及有哪些方法和字段。大概和咱们手动实现的迭代器结构相似。

真正的代码逻辑实在MoveNext方法中执行的,其中有一个大的switch语句。幸运的是,做为一名开发人员不必了解这些细节,但一些迭代器执行的方式仍是值得注意的:

  • 在MoveNext方法第一次执行以前,Current属性老是返回迭代器返回类型的默认的值。例如IEnumeratble返回的是Int32类型,那么默认初始值是0,因此在调用MoveNext方法以前调用Current属性就会返回0。
  • MoveNext方法返回false后,Current属性老是返回最后迭代的那个值。
  • Reset方法通常会抛出异常,而在本文开始代码中,咱们手动实现一个迭代器时在Reset中可以正确执行逻辑。
  • 编译器为咱们产生的嵌套类会同时实现IEnumerator的泛型和非泛型版本(恰当的时候还会实现IEnumerable的泛型和非泛型版本).

   没有正确实现Reset方法是有缘由的--编译器不知道须要使用怎样的逻辑来重新设置迭代器。不少人认为不该该有Reset方法,不少集合并不支持,所以调用者不该该依赖这一方法。

   实现其它接口没有坏处。方法中返回IEnumerable接口,他实现了五个接口(包括IDisposable),做为一个开发者不用担忧这些。同时实现IEnumerable和IEnumerator接口并不常见,编译器为了使迭代器的行为老是正常,而且为可以在当前的线程中仅仅须要迭代一个集合就能建立一个单独的嵌套类型才这么作的。

   Current属性的行为有些古怪,他保存了迭代器的最后一个返回值而且阻止了垃圾回收期进行收集。

所以,自动实现的迭代器方法有一些小的缺陷,可是明智的开发者不会遇到任何问题,使用他可以节省不少代码量,使得迭代器的使用程度比C#1中要广。下面来看在实际开发中迭代器简化代码的地方。

 

 

3.实际开发中使用迭代的例子

 

3.1 从时间段中迭代日期

在涉及到时间区段时,一般会使用循环,代码以下:

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

循环有时没有迭代直观和有表现力,在本例中,能够理解为“时间区间中的每一天”,这正是foreach使用的场景。所以上述循环若是写成迭代,代码会更美观:

foreach(DateTime day in timetable.DateRange)
{
    ……
}

在C#1.0中要实现这个须要下必定功夫。到了C#2.0就变得简单了。在timetable类中,只须要添加一个属性:

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

   只是将循环移动到了timetable类的内部,可是通过这一改动,使得封装变得更为良好。DateRange属性只是遍历时间区间中的每一天,每一次返回一天。若是想要使得逻辑变得复杂一点,只须要改动一处。这一小小的改动使得代码的可读性大大加强,接下来能够考虑将这个Range扩展为泛型Range<T>。

 

3.2迭代读取文件中的每一行

 

读取文件时,咱们常常会书写这样的代码:

using (TextReader reader=File.OpenText(fileName))
{
    String line;
    while((line=reader.ReadLine())!=null)
    {
       ……
    }
}

 

这一过程当中有4个环节:

  • 如何获取TextReader
  • 管理TextReader的生命周期
  • 经过TextReader.ReadLine迭代全部的行
  • 对行进行处理

能够从两个方面对这一过程进行改进:可使用委托--能够写一个拥有reader和一个代理做为参数的辅助方法,使用代理方法来处理每一行,最后关闭reader,这常常被用来展现闭包和代理。还有一种更为优雅更符合LINQ方式的改进。除了将逻辑做为方法参数传进去,咱们可使用迭代来迭代一次迭代一行代码,这样咱们就可使用foreach语句。代码以下:

static IEnumerable<String> ReadLines(String fileName)
{
    using (TextReader reader = File.OpenText(fileName))
    {
        String line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

这样就可使用以下foreach方法来读取文件了:

foreach (String line in ReadLines("test.txt"))
{
    Console.WriteLine(line);
}

   方法的主体部分和以前的同样,使用yield return返回了读取到的每一行,只是在迭代结束后有点不一样。以前的操做,先打开文档,每一次读取一行,而后在读取结束时关闭reader。虽然”当读取结束时”和以前方法中使用using类似,但当使用迭代时这个过程更加明显。

这就是为何foreach迭代结束后会调用迭代器的dispose方法这么重要的缘由了,这个操做可以保证reader可以获得释放。迭代方法中的using语句块相似与try/finally语句块;finally语句在读取文件结束或者当咱们显示调用IEnumerator<String> 的Dispose方法时都会执行。可能有时候会经过ReadLine().GetEnumerator()的方式返回IEnumerator<String> ,进行手动迭代而没有调用Dispose方法,就会产生资源泄漏。一般会使用foreach语句来迭代循环,因此这个问题不多会出现。可是仍是有必要意识到这个潜在的问题。

      该方法封装了前三个步骤,这可能有点苛刻。将生命周期和方法进行封装是有必要的,如今扩展一下,假如咱们要从网络上读取一个流文件,或者咱们想使用UTF-8编码的方法,咱们须要将第一个部分暴漏给方法调用者,使得方法的调用签名大体以下:

static IEnumerable<String> ReadLines(TextReader reader) 

这样有不少很差的地方,咱们想对reader有绝对的控制,使得调用者可以在结束后能进行资源清理。问题在于,若是在第一次调用MoveNext()以前出现错误,那么咱们就没有机会进行资源清理工做了。IEnumerable<String>自身不能释放,他存储了某个状态须要被清理。另外一个问题是若是GetEnumerator被调用两次,咱们本意是返回两个独立的迭代器,而后他们却使用了相同的reader。一种方法是,将返回类型改成IEnumerator<String>,但这样的话,不能使用foreach进行迭代,并且若是没有执行到MoveNext方法的话,资源也得不到清理。

   幸运的是,有一种方法能够解决以上问题。就像代码没必要当即执行,咱们也不须要reader当即执行。咱们能够提供一个接口实现“若是须要一个TextReader,咱们能够提供”。在.NET 3.5中有一个代理,签名以下:

public delegate TResult Func<TResult>()

代理没有参数,返回和类型参数相同的类型。咱们想得到TextReader对象,因此可使用Func<TextReader>,代码以下:

using (TextReader reader=provider())
{
    String line;
    while ((line=reader.ReadLine())!=null)
    {
        yield return line;
    }         
}

 

3.3 使用迭代块和迭代条件来对集合进行进行惰性过滤

   LINQ容许对内存集合或者数据库等多种数据源用简单强大的方式进行查询。虽然C#2没有对查询表达式,lambda表达及扩展方法进行集成。可是咱们也能达到相似的效果。

   LINQ的一个核心的特征是可以使用where方法对数据进行过滤。提供一个集合以及过滤条件代理,过滤的结果就会在迭代的时候经过惰性匹配,每匹配一个过滤条件就返回一个结果。这有点像List<T>.FindAll方法,可是LINQ支持对全部实现了IEnumerable<T>接口的对象进行惰性求值。虽然从C#3开始支持LINQ,可是咱们也可使用已有的知识在必定程度上实现LINQ的Where语句。代码以下:

public static IEnumerable<T> Where<T>(IEnumerable<T> source, Predicate<T> predicate)
{
    if (source == null || predicate == null)
        throw new ArgumentNullException();
    return WhereImpl(source, predicate);
}

private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Predicate<T> predicate)
{
    foreach (T item in source)
    {
        if (predicate(item))
            yield return item;
    }
}

IEnumerable<String> lines = ReadLines("FakeLinq.cs");
Predicate<String> predicate = delegate(String line)
{
    return line.StartsWith("using");
}; 

    如上代码中,咱们将整个实现分为了两个部分,参数验证和具体逻辑。虽然看起来奇怪,可是对于错误处理来讲是颇有必要的。若是将这两个部分方法放到一个方法中,若是用户调用了Where<String>(null,null),将不会发生任何问题,至少咱们期待的异常没有抛出。这是因为迭代块的惰性求值机制产生的。在用户迭代的时候第一次调用MoveNext方法以前,方法主体中的代码不会执行,就像在2.2节中看到的那样。若是你想急切的对方法的参数进行判断,那么没有一个地方可以延缓异常,这使得bug的追踪变得困难。标准的作法如上代码,将方法分为两部分,一部分像普通方法那样对参数进行验证,另外一部分代码使用迭代块对主体逻辑数据进行惰性处理。

    迭代块的主体很直观,对集合中的逐个元素,使用predict代理方法进行判断,若是知足条件,则返回。若是不知足条件,则迭代下一个,直到知足条件为止。若是要在C#1中实现这点逻辑就很困难,特别是实现其泛型版本。

   后面的那段代码演示了使用以前的readline方法读取数据而后用咱们的where方法来过滤获取line中以using开头的行,和用File.ReadAllLines及Array.FindAll<String>实现这一逻辑的最大的差异是,咱们的方法是彻底惰性和流线型的(Streaming)。每一次只在内存中请求一行并对其进行处理,固然若是文件比较小的时候没有什么差异,可是若是文件很大,例如上G的日志文件,这种方法的优点就会显现出来了。

 

4 总结

   C#对许多设计模式进行了间接的实现,使得实现这些模式变得很容易。相对来针对某一特定的设计模式直接实现的的特性比较少。从foreach代码中看出,C#1对迭代器模式进行了直接的支持,可是没有对进行迭代的集合进行有效的支持。对集合实现一个正确的IEnumerable很耗时,容易出错也很很枯燥。在C#2中,编译器为咱们作了不少工做,为咱们实现了一个状态机来实现迭代。

    本文还展现了和LINQ类似的一个功能:对集合进行过滤。IEnumerable<T>在LINQ中最重要的一个接口,若是想要在LINQ To Object上实现本身的LINQ操做,那么你会由衷的感叹这个接口的强大功能以及C#语言提供的迭代块的用处。

    本文还展现了实际项目中使用迭代块使得代码更加易读和逻辑性更好的例子,但愿这些例子使你对理解迭代有所帮助。

相关文章
相关标签/搜索