zoukankan      html  css  js  c++  java
  • .Net编程接口剖析系列之迭代器zz

    在上一篇文章中,我们讲了IComparable和IComparer接口,而在本篇文章中,我要给大家讲讲用于枚举元素集合的两个接口 IEnumerator和IEnumerable。IEnumerator用于实现一个迭代器(相当于以前C++的文章中所说的iterator),它具 备列举一个数据结构中所有元素所需要的一些方法和属性。而IEnumerable接口则用于返回一个迭代器对象,一个实现了IEnumerable的类型 表示这个类型对象是可以枚举的。
        下面我们先来看看这两个接口的定义吧!

    IEnumerator

         迭代器用于枚举一个数据结构中的所有元素。
     namespace System.Collections 
    {
    [ComVisible(true)]
    [Guid("496B0ABE-CDEE-11d3-88E8-00902754C43A")]
    public interface IEnumerable
    {
    [DispId(-4)]
    IEnumerator GetEnumerator();
    }
    }
       从上面的定义我们可以看到,一个Emurator具备了枚举一个数据结构中所有元素的最基本能力:
       获取当前元素的的能力:Current属性;
       移动元素指针的能力:MoveNext方法; 
       重置迭代器的能力:Reset方法。
       这里的Current属性是Object类型,也就是可以返回所有类型元素,而与之对应的泛型接口 是:System.Collections.Generic.IEnumerator<T>,它除了继承了Ienumerator之外,还增 加了一个特定类型的Current属性。
    IEnumerable 
       IEnumerable声明一个类型为可枚举的类型,而它的定义很简单,就是返回一个迭代器。
    namespace System.Collections 
    {
    [ComVisible(true)]
    [Guid("496B0ABE-CDEE-11d3-88E8-00902754C43A")]
        我们应该对C#风格的遍历语法应该很熟悉了,也就是foreach语句。说到foreach这个东西,其实在C++中也存在,但是是以函数的形式做在库里 面的,而对C#来说,它已经被做到语言中去了。在绝大多数情形下,我们应该尽量使用foreach语句来遍历一个集合对象,而不是自己写一个for循环或 者其他的while循环等,理由很简单:效率。而foreach语法需要被枚举的对象类型实现了IEnumerable接口。
        与IEnumerable对应的泛型接口是:System.Collections.Generic.IEnumerable<T>。
    设计一个集合类

        通常,IEnumerator和IEnumerable是一起使用的。假设我们设计一个属于自己的一个数据结构类MyCollection,并且让他可以被枚举,那么整体上应该怎么设计呢?我们看看下面的代码。
    class MyCollection:IEnumerable 
    {
    public struct MyEmurator : IEnumerator
    {
    //此处省略实现代码
    }
    //此处省略部分实现代码
    public IEnumerator GetEnumerator()
    {
    return new MyEmurator(this);
    }
    }
       这是一个典型的对IEnumerator和IEnumerable的应用方式。几乎所有的System.Collection里面的容器都是都是这样来设 计的。将容器类型本身实现IEnumerable,表明容器是可枚举的。而迭代器类型则是一个嵌套类型,通过容器类的接口函数GetEnumerator 来返回迭代器的实例。通常一个容器和它的迭代器是紧密相关的,并且一个容器配备一个迭代器已经足以,那么将迭代器定义为嵌套类型,避免了管理的混乱。

    实现一个2D List类型

        我们这里说的二维List类型,其实就是实现一个以List为元素的List,简而言之,这个List2D就是用来存放List的一个List;但是我们枚举的时候,并不想要枚举List2D里面的list, 而是想直接枚举list里面的元素。
        我们这里定义了一个泛型类List2D<T> ,实现了IEnumerable<T>接口。这里为了精简代码,这里没有让List2D实现IList接口,只提供了Add、Clear等几个简单方法。不多说了,还是来看看下面的代码吧!
    class List2D<T> : IEnumerable<T> 
    {
    //内嵌迭代器类型
    public struct Emurator : IEnumerator<T>
    {
    //此处代码先不写出
    }
    private List<List<T>> _lists=new List<List<T>>();//存储列表

    public List<T> this[int index]
    {
    get { return _lists[index]; }
    }

    public int Count
    {
    get
    {
    int count = 0;
    foreach (List<T> list in _lists)
    {
    count += list.Count;
    }
    return count;
    }
    }

    public void Add(List<T> item)
    {
    _lists.Add(item);
    }

    public void Clear()
    {
    _lists.Clear();
    }

    #region IEnumerable Members

    public IEnumerator GetEnumerator()
    {
    return ((IEnumerable<T>)this).GetEnumerator();
    }
    #endregion

    #region IEnumerable<T> Members

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
    return new Emurator(this);
    }
    #endregion
    }
    我们再来看看迭代器类型struct Emurator的定义,它是嵌套在List2D之中的,它实现了IEnumerator<T>接口。迭代器的实现详见注释。
    class List2D<T> : IEnumerable<T> 
    {
    public struct Emurator : IEnumerator<T>
    {
    List2D<T> _list2D; //被枚举的2D list
    IEnumerator<List<T>> _listsEmuretor; //列表迭代器
    IEnumerator<T> _listEmuretor; //元素迭代器
    bool _started; //是否开始枚举
    T _current; //当前元素

    public Emurator(List2D<T> list2D)
    {
    _list2D=list2D;
    _listsEmuretor = list2D._lists.GetEnumerator();
    _listEmuretor=default(IEnumerator<T>);
    _started = false;
    _current = default(T);
    }

    #region IEnumerator Members

    public object Current
    {
    get { return _current; }
    }

    public bool MoveNext()
    {
    if (!_started) //第一次MoveNext, 需要取第一个列表
    {
    _started = true;
    if (!_listsEmuretor.MoveNext())
    return false;

    _listEmuretor = _listsEmuretor.Current.GetEnumerator(); //获取第一个list的迭代器
    }

    while(true)
    {
    if (!_listEmuretor.MoveNext())
    {
    //当前列表枚举结束,需要移动到一个列表
    if (!_listsEmuretor.MoveNext())
    return false; //所有列表遍历完毕,返回false

    _listEmuretor = _listsEmuretor.Current.GetEnumerator();
    }
    else //当前列表还有元素,成功
    {
    _current = _listEmuretor.Current;
    return true;
    }
    }
    }

    public void Reset()
    {
    _listsEmuretor.Reset();
    _current = default(T);
    _started = false;
    }
    #endregion

    #region IEnumerator<T> Members
    T IEnumerator<T>.Current
    {
    get { return _current; }
    }
    #endregion

    public void Dispose()
    {
    }
    }
    }
        真不容易,写了好些代码才把这个2D List的迭代器实现。我们在Main函数里面写一些测试代码,看看它能否正常运行。

    static void Main(string[] args) 
    {
    List2D<string> list2D = new List2D<string>();//2维string列表

    List<string> list1 = new List<string>();
    list1.Add("list1-1");
    list1.Add("list1-2");
    list1.Add("list1-3");
    list2D.Add(list1); //第一个列表有3个元素

    List<string> list2 = new List<string>();
    list2D.Add(list2);//第二个列表没有元素

    List<string> list3 = new List<string>();
    list1.Add("list3-1");
    list1.Add("list3-2");
    list2D.Add(list3); //第三个列表有2个元素

    foreach (string str in list2D)//枚举所有string
    {
    Console.WriteLine(str);
    }

    Console.ReadKey();
       运行结果如下。
       list1-1
       list1-2
       list1-3
       list3-1
       list3-2
       我们可以看到运行结果完全正确。
    Yield Return

        从前面我们可能发现,有时候写一个迭代器可能是挺麻烦的一个事情,其实需求却可能是挺简单的,但是我们却要写大量代码来实现一个迭代器。所幸的是,C#给 我们提供一个独门利器:Yield Return! 利用yield return,我们不需要写一个迭代器就能够实现GetEnumerator函数了。
    Yield return不同于普通函数的Return,它从作用上来说,相当于每调用一次yield return,就返回一个被枚举的元素。Yield Return语法只能用在返回值类型为IEnumerator的函数中。 我们现在来看看,List2D的GetEnumerator函数如何用yield return来实现,从而使我们不用再创建一个自定义的Emurator类型。
     IEnumerator<T> IEnumerable<T>.GetEnumerator() 
    {
    foreach (List<T> list in _lists)
    {
    foreach (T item in list)
    {
    yield return item;
    }
    }
    }
        7行!包括4个花括弧仅仅用了7行代码,就实现了一个迭代器的所有功能,而前面我们为了实现一个迭代器,写了数十行的代码,这对编码效率上来说,这是多么 大的一个提升啊。当然,并非所有情形下,用yield return都是合适,但是在一般的需求之下,它是最好的选择。
    习惯了传统编程语言的 人,可能会比较难易理解Yield Return这个东西,而且更会对它是到底是如何实现的充满了疑问。其实Yield Return是C#从语言层面上提供的一种简化代码的语法形式而已,而归根到底,其实是C#的编译器辅助我们完成了一部分编码工作—它为我们自动创建了一 个匿名的迭代器类型,并且用那个迭代器实现了我们在使用yield return语义所实现的迭代功能。
        如果你对上面的论断产生怀疑的话,我们可以借助VS.net自带的IL查看工具ildasm.exe来查看我们用yield return代码编译后的exe文件。Ildasm.exe (VS2005)通常存放在“C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\”目录下。
        从下面Ildasm.exe的截图中我们可以看见List2D类型中有个嵌套类型GetEnumerator>d__0<T>,而这个恰恰就是C#编译器为我们生成的匿名迭代器类型。

    在感叹C#编译器的智能的时候,我们有必要对yield return的实现做一个更加深入的了解,那就是查看它自动生成的迭代器的代码究竟是怎么样的,与我们自己写的迭代器相比,究竟有多大差别。使用 ildasm可以查看IL代码,不过这是非常痛苦的一件事情,即使你对IL的所有指令都背得滚瓜烂熟,同样也是一间非常痛苦的事情。所以,这里我想向大家 推荐一个代码学习利器,.Net的反编译器“.Net Reflector”,它可以将编译好的.Net程序反编译为C#代码,并且有着比较高的还原率,而且这个工具本身还在不断升级当中。你可以从 http://www.aisto.com/roeder/dotnet免费下载这个软件
        Yield Return可以在GetEnumerator函数中任意地方使用,而我们的程序结构一般会包括顺序结构、循环和分支选择等。为了弄清编译器对yield return的处理细节,我们先设计一个简单的类,它仅仅使用yield return先返回一个-1,再返回一个100,然后就结束了,这是一个再简单不过的顺序执行结构了,下面是这个类的代码:
    class TestYieldReturn:IEnumerable 
    {
    public IEnumerator GetEnumerator()
    {
    yield return -1;
    yield return 100;
    }
    }
        我们将这段程序编译好,然后用.NET Reflactor将其进行反编译,于是我们得到了编译器自动生成的迭代器的代码:
    [CompilerGenerated] 
    private sealed class <GetEnumerator>d__0 : IEnumerator<object>, IEnumerator, IDisposable
    {
    // Fields
    private int <>1__state;
    private object <>2__current;
    public TestYieldReturn <>4__this;

    // Methods
    [DebuggerHidden]
    public <GetEnumerator>d__0(int <>1__state)
    {
    this.<>1__state = <>1__state;
    }

    private bool MoveNext()
    {
    switch (this.<>1__state)
    {
    case 0:
    this.<>1__state = -1;
    this.<>2__current = -1;//首先返回-1
    this.<>1__state = 1;
    return true;

    case 1:
    this.<>1__state = -1;
    this.<>2__current = 100;//再返回100
    this.<>1__state = 2;
    return true;

    case 2:
    this.<>1__state = -1; //已经到了末尾,设置状态为结束
    break;
    }
    return false;
    }

    [DebuggerHidden]
    void IEnumerator.Reset()
    {
    throw new NotSupportedException();
    }

    void IDisposable.Dispose()
    {
    }

    // Properties
    object IEnumerator<object>.Current
    {
    [DebuggerHidden]
    get
    {
    return this.<>2__current;
    }
    }
    object IEnumerator.Current
    {
    [DebuggerHidden]
    get
    {
    return this.<>2__current;
    }
    }
    }
        我们能看到这个迭代器类型的前面有个Attribute:[CompilerGenerated],这表明这个迭代器确实属于编译器生成无疑。迭代器的成 员变量除了current和被枚举对象“<>4__this”之外,还增加了一个“<>1__state”的成员,而这个 state,真是用于实现顺序程序结构的状态变量,我们可以分析一下最重要的MoveNext函数,State的0、1、2分别代表第一个yield return之前,第一个yield return之后,第二个yield return之后的程序状态,而-1代表枚举结束。编译器就是用state成员变量来控制顺序逻辑的先后次序。
        类似的,C#编译器通过设定一些其他的辅助变量,来实现循环和分支等控制结构。有时候这几种结构是相互组合的,编译器都能够很好的实现他们。
    Version控制

        我们来看看这段代码,它想把列表中的所有负数给删除掉。
    List<int> list=new List<int>(); 
    list.Add(0);
    list.Add(-1);
    list.Add(2);

    foreach(int i in list)
    {
    if(i<0) list.Remove(i);
    }
         这段代码看起来似乎写的没有问题,编译也能通过。但是真正跑起来就会抛出异常了。它将会抛出一个InvalidOperationException,错 误信息是"Collection was modified; enumeration operation may not execute."。
        怎么会这样呢?原来是微软在定义迭代器功能的时候,要求在枚举开始到结束的这个过程之中,应该要求保证被枚举的对象不能够增加或者减少元素。其实这个需求也很好理解,相信谁也不想在清点队伍人数的时候,队伍里面的人还在出出进进吧?
        这里善于思考的读者可能马上会想到一个问题,迭代器又是如何知道这个集合发生改变了呢?这里微软用了一个简单而巧妙的办法,给集合对象设置了一个版本信息,通过在迭代过程中不断检查版本信息,来判断集合对象是否已经被更新。具体的做法如下。
    1. 给集合对象增加一个成员private int _version,初始值设置为0;
    2. 在所有直接对集合元素进行更新的操作中,例如insert、remove等等,都将_version ++;
    3. Enumerator也设置一个_version成员,并且在构造函数中将其设置为集合对象的_version成员。其实也就是记录了collection当时的版本。
    4. Enumerator在迭代的过程中,不断检查collection的_version字段,一旦发现与当初记录的_version值不同,则认定为集合已经被更新,立即抛出异常。

    一点感言

        从foreach到yield return还有C#3.X的LINQ等等,我们发现C#正在为我们越来越多的事情,但是我们不知道这种变化何时是尽头,也不知道我们的学习何时是尽头。 C#引入这些新特性是希望不断简化我们的编码工作,但是与此同时难免也会带来一些新的问题,那就是C#正在变得越来越庞大和复杂,也许有一天我们会发现, 自己都不知道什么是C#了,也许把什么都做入语言层面并非是一件好事。天天有新技术的感觉是不错,不过昨天的新技术在今天立马就变成了旧技术却可能是一件 令人沮丧的事情。我们需要创新,但是更加需要经典,C#何时才能成为经典?
  • 相关阅读:
    Oracle:Using the DBMS_STATSpackage
    Oracle partitioning is not always a good idea.
    Oracle: Benefits and consequences of the NOLOGGING option
    Oracle :Insert ways.
    Oracle:临时表的统计信息
    C#中使用DTS来导入数据及相关问题
    [收藏]CSS网页制作时实现自动换行的小技巧
    新加了牛人的Blog链接
    在.Net下使用Access 的日期类型 及与js的日历控件交互
    在程序中生成PDF
  • 原文地址:https://www.cnblogs.com/end/p/2152969.html
Copyright © 2011-2022 走看看