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#何时才能成为经典?
  • 相关阅读:
    动态生成 Excel 文件供浏览器下载的注意事项
    JavaEE 中无用技术之 JNDI
    CSDN 泄露用户密码给我们什么启示
    刚发布新的 web 单点登录系统,欢迎下载试用,欢迎提建议
    jQuery jqgrid 对含特殊字符 json 数据的 Java 处理方法
    一个 SQL 同时验证帐号是否存在、密码是否正确
    PostgreSQL 数据库在 Windows Server 2008 上安装注意事项
    快速点评 Spring Struts Hibernate
    Apache NIO 框架 Mina 使用中出现 too many open files 问题的解决办法
    解决 jQuery 版本升级过程中出现 toLowerCase 错误 更改 doctype
  • 原文地址:https://www.cnblogs.com/end/p/2152969.html
Copyright © 2011-2022 走看看