Hello,Coders。我们除了天天的码 if…else…之外,还会不断的码出foreach。我今天要说的是:传统遍历需实现的接口及我们还有一种更简洁优雅的方式实现多种迭代器。

传统遍历

传统的遍历即通过让集合类实现IEnumerable、IEnumerator或IEnumerable<T>、IEnumerator<T>接口来支持遍历。

1
2
3
4
5
6
7
8
9
10
11
public interface IEnumerable   // 可枚举接口
{
    IEnumeratorGetEnumerator();
}
 
public interface IEnumerator    // 枚举器接口
{
    object Current { get; }
    boolMoveNext();
    void Reset();
}
  1. 分析:

1)         从这两个接口的用词选择上,也可以看出其不同:

a)         IEnumerable是一个声明式的接口,声明实现该接口的类是可枚举。

b)         IEnumerator是一个实现式的接口,IEnumerator对象说明如何实现枚举器。

2)         Foreach语句隐式调用集合的无参GetEnumerator方法(不论集合是否有实现IEnumerable接口,但只要有无参GetEnumerator方法并返回IEnumerator就可遍历)。

3)         集合类为什么不直接实现IEnumerable和IEnumerator接口?

这样是为了提高并发性。Eg:一个遍历机制只有一个Current,一旦并发就会出错。然而“将遍历机制与集合分离开来”如果要实现同时遍历同一个集合,只需由集合IEnumerable.GetEnumerator() 返回一个新的包含遍历机制(IEnumerator)的类实例即可。

  1. 调用过程

插播一段:由foreach执行过程可知其迭代器是延迟计算的。

因为迭代的主体在MoveNext() 中实现,foreach中每次遍历执行到 in 的时候才会调用MoveNext() ,所以其迭代器耗时的指令是延迟计算的。

延迟计算(Lazy evaluation):来源自函数式编程,在函数式编程里,将函数作为参数来传递,传递过程中不会执行函数内部耗时的计算,直到需要这个计算结果的时候才调用,这样就可以因为避免一些不必要的计算而改进性能。 (另外还有linq、DataReader等也运用了延迟计算的思想)

  1. 具体实现示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/// <summary>
/// 课程
/// </summary>
public class Course
{
    public Course(String name)
    {
        this.name = name;
    }
 
    private String name = string.Empty;
    public String Name
    {
        get { return name; }
    }
}
 
public class CourseCollection : IEnumerable<Course>
{
    public CourseCollection()
    {
        arr_Course = new Course[]
        {
            new Course("语文"),
            new Course("数学"),
            new Course("英语"),
            new Course("体育")
        };
    }
 
    private Course[] arr_Course;
 
    public Course this[int index]
    {
        get { return arr_Course[index]; }
    }
 
    public int Count
    {
        get { return arr_Course.Length; }
    }
 
    public IEnumerator<Course> GetEnumerator()
    {
        return new CourseEnumerator(this);
    }
 
    #region 实现 IEnumerable<T>
 
    private sealed class CourseEnumerator : IEnumerator<Course>
    {
        private readonly CourseCollection courseCollection;
        private int index;
 
        internal CourseEnumerator(CourseCollection courseCollection)
        {
            this.courseCollection = courseCollection;
            index = -1;
        }
 
        public Course Current
        {
            get { return courseCollection[index]; }
        }
 
        bool IEnumerator.MoveNext()
        {
            index++;
            return (index < courseCollection.Count);
        }
 
        void IEnumerator.Reset()
        {
            index = -1;
        }
         
        ……
    }
 
    #endregion
 
    ……
}

有了对“传统遍历”实现方式的理解才能快速明白下一节“迭代器”的实现原理。要知道绝大部分最新的概念其实都可以用最简单的那些概念组合而成。而只有对基本概念理解,才能看清那些复杂概念的实质。

迭代器(iterator)

迭代器是 C# 2.0 中的新功能。它使类或结构支持foreach迭代,而不必“显示”实现IEnumerable或IEnumerator接口。只需要简单的使用 yield 关键字,由 JIT 编译器帮我们编译成实现 IEnumerable或IEnumerator 接口的对象(即:本质还是传统遍历,只是写法上非常简洁)。

对于本节提到的编译后的代码,可通过 Reflector.exe , ILSpy.exe 进行查看。

  1. 分析

1)         yield 语句只能出现在 iterator 块(迭代块)中,该块只能用作方法、运算符或get访问器的主体实现。这类方法、运算符或访问器的“主体”受以下约束的控制:

a)         不允许不安全块。

b)         方法、运算符或访问器的参数不能是 ref 或 out。

2)         迭代器代码使用 yield return 语句依次返回每个元素。yield break 将终止迭代。

a)       yield return 的时候会保存当前位置(状态机)并把控制权从迭代器中交给调用的程序,做必要的返回值处理,下一次进入迭代器将从之前保存的位置处开始执行直到迭代结束或调用yield break。

b)         yield break 就是控制权交给调用程序就不回来了从而终止迭代。

3)         yield return 语句不能放在 try-catch 块中。但可放在后跟 finally 块的 try 块中。

4)         yield break 语句可放在 try 块或 catch 块中,但不能放在 finally 块中。

5)         yield 语句不能出现在匿名方法中。

6)         迭代器必须返回相同类型的值,因为最后输出为IEnumerator.Current是单一类型。(见下面示例)

7)         在同一个迭代器中可以使用多个 yield 语句。(见下面示例)

8)         自定义迭代器:迭代器可以自定义名称、可以带参数,但在foreach中需要显示去调用自定义的迭代器。(见下面示例)

9)         迭代器的返回类型必须为IEnumerator、IEnumerator<T>或IEnumerable、IEnumerable<T>。(见下面示例)

  1. 迭代器的具体实现

1)         返回类型为IEnumerator、IEnumerator<T>

返回此类型的迭代器方法必须满足:

a)         必须有GetEnumerator且不带参数;

b)         必须是public公共成员;

见示例代码,我们将CourseCollection集合对象的IEnumerable.GetEnumerator() 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 注意:返回什么,泛型就为什么类型
public IEnumerator<String> GetEnumerator()
{
    for (int i = 0; i < arr_Course.Length; i++)
    {
        Course course = arr_Course[i];
        yield return "选修:" + course.Name;
 
        // 两个 yield return
        yield return Environment.NewLine;
 
        // 每个yield return 保存当前位置并返回值,下一次进入迭代器时将从之前保存的位置处开始执行
        if (String.Compare(course.Name, "体育") == 0)
            yield break;
 
        List<string> strs=new List<string>{"435435","546546"};
        foreach (string s in strs)
        {
            Console.WriteLine(s);
        }
    }
}

经过 JIT 编译后,会自动生成一个实现了 IEnumerator<String> 接口的对象。具体代码可通过 Reflector 工具查看,下面展示其中的MoveNext() 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private bool MoveNext()
{
    switch (this.<>1__state)
    {
        case 0:
            this.<>1__state = -1;
            this.<i>5__2 = 0;
            while (this.<i>5__2 < this.<>4__this.arr_Course.Length)
            {
                this.<course>5__3 = this.<>4__this.arr_Course[this.<i>5__2];
                this.<>2__current = "选修:" + this.<course>5__3.Name;
                this.<>1__state = 1;
                return true;
            Label_007C:
                this.<>1__state = -1;
                this.<>2__current = Environment.NewLine;
                this.<>1__state = 2;
                return true;
            Label_009C:
                this.<>1__state = -1;
                if (string.Compare(this.<course>5__3.Name, "体育") == 0)
                {
                    break;
                }
                this.<>g__initLocal0 = new List<string>();
                this.<>g__initLocal0.Add("435435");
                this.<>g__initLocal0.Add("546546");
                this.<strs>5__4 = this.<>g__initLocal0;
                foreach (string s in this.<strs>5__4)
                {
                    Console.WriteLine(s);
                }
                this.<i>5__2++;
            }
            break;
 
        case 1:
            goto Label_007C;
 
        case 2:
            goto Label_009C;
    }
    return false;
}

通过代码,我们可以知道:

a)         同一个迭代器中有多少个 yield return语句,while 循环中就有多少个 return true 。

b)       yield retuen结束本次循环,yield break结束整个循环。输出数据的顺序通过生成类中的一个state状态字段做为switch 标识来决定要输出第几个 yield return 。yield return在每个case里面改变state内部字段,使正确执行完多个return返回数据,并最后通过return true来结束本次MoveNext()。而yield break语句直接生成break并重置state状态字段为switch中没有的值而跳出switch语句,通过执行最后的return false来结束整个循环。

c)    注意:yield return 后面的 List<string>代码段也会被执行。

2)         返回类型为IEnumerable、IEnumerable<T>

返回此类型的迭代器必须满足:

a)         必须可以在foreach语句中被调用(访问权限);

返回此类型的迭代器通常用于实现自定义迭代器,即:迭代器可以自定义名称、可以带参数。Eg:(升序和降序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public IEnumerable<String> GetEnumerable_ASC()
{
    return this;
}
 
public IEnumerable<String> GetEnumerable_DESC()
{
    for (inti = arr_Course.Length - 1; i>= 0; i--)
    {
        Course course = arr_Course[i];
        yield return "选修:" +course.Name;               
        yield return Environment.NewLine;
 
    }
}

需如下进行迭代器调用:

1
2
3
yield_Example.CourseCollection col2 = new yield_Example.CourseCollection();
foreach (String str in col2.GetEnumerable_ASC()){ // col2.GetEnumerable_ASC()}
foreach (String str in col2.GetEnumerable_DESC()){//col2.GetEnumerable_DESC()}

经过 JIT 编译后,会自动生成一个直接实现IEnumerator<String>和IEnumerator<String>接口的对象,其GetEnumerator() 方法返回自己this(因为本身实现了IEnumerator接口)。

疑问:没有“将遍历机制与集合分离开来”?

这是因为在不同foreach遍历中所访问的由编译器自动生成的迭代器具有其自己独立的状态,所以迭代器之间互不影响,不存在并发的问题。

OVER,谢谢观看。(如有缺漏或错误请留言,谢谢!!!)

C#稳固基础:传统遍历与迭代器的更多相关文章

  1. python基础(8)--迭代器、生成器、装饰器

    1.迭代器 迭代器是访问集合元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中往后退.另外,迭代器的一大优 ...

  2. Java基础知识强化之集合框架笔记07:Collection集合的遍历之迭代器遍历

    1. Collection的迭代器: Iterator iterator():迭代器,集合的专用遍历方式 2. 代码示例: package cn.itcast_03; import java.util ...

  3. 【笨木头Lua专栏】基础补充04:迭代器初探

    今天学习的内容还蛮有意思的,让我兴奋了一下~ 笨木头花心贡献,哈?花心?不,是用心~ 转载请注明,原文地址: http://www.benmutou.com/archives/1714 文章来源:笨木 ...

  4. java 15-4 集合的专用遍历工具 迭代器

    Iterator iterator():迭代器,集合的专用遍历方式 A:Object next():获取元素,并移动到下一个位置. 有时候会出现这样的错误: NoSuchElementExceptio ...

  5. Python开发【第一篇】Python基础之生成器和迭代器

    生成器和迭代器 1.生成器 一个函数调用时返回一个迭代器,那这个函数就叫做生成器(generator):如果函数中包含yield语法,那这个函数就会变成生成器: def func(): yield 1 ...

  6. Python-Day4 Python基础进阶之生成器/迭代器/装饰器/Json & pickle 数据序列化

    一.生成器 通过列表生成式,我们可以直接创建一个列表.但是,受到内存限制,列表容量肯定是有限的.而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面 ...

  7. python基础(八)-迭代器与生成器

    一.迭代器 li=[1,2,3] f=li.__iter__() print(f) print(f.__next__()) print(f.__next__()) print(f.__next__() ...

  8. Java之集合的遍历与迭代器

    集合的遍历 依次获取集合中的每一个元素 将集合转换成数组,遍历数组 //取出所有的学号, 迭代之后显示学号为1004-1009 Object[] c=map.keySet().toArray();// ...

  9. python基础---列表生成器、迭代器等

    一.列表生成式 用来创建list的表达式,相当于for循环的简写形式 语法: [表达式 for循环 判断条件] ''' 普通写法 ''' def test(): l= [] for i in rang ...

随机推荐

  1. shell脚步传参

    linux系统除了提供位置参数还提供内置参数,内置参数如下: $# ----传递给程序的总的参数数目 $? ----上一个代码或者shell程序在shell中退出的情况,如果正常退出则返回0,反之为非 ...

  2. 自动显示隐藏布局的listView

    借助View的OnTouchListener接口来监听listView的滑动,通过比较与上次坐标的大小,判断滑动方向,并通过滑动方向来判断是否需显示或者隐藏对应的布局,并且带有动画效果. 1.自动显示 ...

  3. 《C#高级编程》读书笔记

    <C#高级编程>读书笔记 C#类型的取值范围 名称 CTS类型 说明 范围 sbyte System.SByte 8位有符号的整数 -128~127(−27−27~27−127−1) sh ...

  4. TFS 10周年生日快乐 – TFS与布莱恩大叔的故事

    今天看了一下Brian Harry大叔的博客,才发现2016年3月17日,是Team Foundation Server的10岁生日. Today marks the 10th anniversary ...

  5. C语言链表实现约瑟夫环问题

    需求表达:略 分析: 实现: #include<stdio.h> #include<stdlib.h> typedef struct node { int payload ; ...

  6. Spring MVC拦截器+注解方式实现防止表单重复提交

    原理:在新建页面中Session保存token随机码,当保存时验证,通过后删除,当再次点击保存时由于服务器端的Session中已经不存在了,所有无法验证通过. 注,如果是集群的方式,则需要将token ...

  7. 集群(cluster)和高可用性(HA)的概念

    1.1 什么是集群 简单的说,集群(cluster)就是一组计算机,它们作为一个整体向用户提供一组网络资源.这些单个的计算机系统就是集群的节点(node).一个理想的集群是,用户从来不会意识到集群系统 ...

  8. python requests 安装

    在 windows 系统下,只需要输入命令 pip install requests ,即可安装. 在 linux 系统下,只需要输入命令 sudo  pip install requests ,即可 ...

  9. 使用开发者工具调试jsp页面中的脚本

    只举例火狐和谷歌.如果是火狐,一般是用firebug,首先确保开启脚本调试: 然后刷新一下要调试的页面,点击firebug中的行内,选择当前页面: js文件一般直接显示的是js文件的名字,而页面一般是 ...

  10. 不懂前端的程序员不是好美工——UI框架metronic使用教程——程序员视角

    本着不懂前端的程序员不是好美工的观点,所以作为一个仅懂一点前端的程序员,为了成为一个好美工,所以只能用些取巧的方法伪装一下. metronic一个基于bootstrap的响应式的后台管理平台的UI框架 ...