C#迭代器的实现和应用(一)——基础篇

本篇文章主要展示C#迭代器的实现和yield关键字的使用。
在熟悉了这些C#迭代器的基础之后,我会在后面两篇博客中展示迭代器的延迟处理和LINQ中流处理的实现,以及Unity中的协程的实现,如果你已经对这部分内容熟悉了,可以略过本篇,直接进入后面两篇文章。

C#迭代器的实现和应用(二)——延迟执行、流式处理与两个基本LINQ扩展的实现
C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程

一、从设计模式说起

设计模式是现在所有程序员在进阶时都应该学习的知识,其中的迭代器模式是一个很不起眼但又被广泛使用的模式,有很多语言都原生支持了这种模式,包括C#、C++、JAVA、Lua等等等等。
迭代器模式提供了不需要考虑集合的实际内部结构条件下对集合中对象进行遍历的能力,我以前也实现过lua的泛型迭代器:《Lua的for泛型迭代器使用方式》
关于迭代器模式的详细介绍和实现可以查看这里:《迭代器模式|菜鸟教程》。菜鸟教程里的代码是JAVA中的实现,在C#中的实现又有所区别,并且由于语法原生的支持和优化,C#中的迭代器可用性变得更强,其他的应用也变得非常强大。

九层之台,起于垒土,我们还是从最基础的内容开始,在C#中实现一个最基础的、没有语法糖优化的迭代器。

二、C#迭代器的完整实现

与前面设计模式的文章一样,在C#中,迭代操作也是由两部分组成——迭代器迭代函数,多数情况下,C#中使用foreach关键字来对迭代器进行迭代,因此,为了拆解内部实现,我们有必要也实现一个C#的迭代函数。

1. C#中的迭代器类图

在C#中自带几个迭代器接口,其使用与设计模式中所说的完全一致,类图如下:
在这里插入图片描述
简单来说迭代器分为两部分:

  1. 集合要实现IEnuerable接口,实现GetEnumerator方法,这个成员方法会返回一个IEnumerator对象,这个对象就是我们的迭代器;
  2. 迭代器实现IEnumerator接口,这个接口只有三个方法:MoveNext方法用于判断是否已经到达了迭代的终点,Current属性用于获取当前迭代的值,Reset方法用于重置迭代器。
  3. 此外,这两个类分别还有对应的泛型类。IEnumerator<T>为了实现对特定对象的释放,又额外继承了IDisposable接口。
2. foreach的实现

光有了迭代器也不行,我们当然还需要一个自己的迭代函数,在C#中原生提供了foreach作为关键字,以foreach(Type value in IEnumerable)的形式进行迭代,集合中每一个值都会被转换成Type,通过value直接获取即可。
注意!如果使用的是非泛型的对象,这里会使用强制转换,类型不安全,并且如果集合中的对象是值类型,也会发生一个拆箱操作,造成额外的性能问题。

foreach的实现其实很简单,如设计模式中所示,对集合调用GetEnumerator方法获取迭代器,然后使用while循环,直到迭代器的MoveNext方法返回false,当MoveNext方法返回的值不为false时,可以对迭代器调用Current属性,获取迭代器当前所对应的值。

实现代码如下,为了实现对数据的操作,我传入了一个Action<T>作为回调用于处理。

注意!这里我仅仅实现了相同的迭代行为,实际使用的foreach实现要更复杂,为了体现核心思想,数据的判空、错误处理等全部被我省略了。

        public static void Foreach<T>(this IEnumerable<T> list, Action<T> action)
        {
            var it = list.GetEnumerator();
            while (it.MoveNext())
            {
                action(it.Current);
            }
        }

当然也要进行测试:

int[] numbers = { 1, 2, 3, 4, 5, 6 };
numbers.Foreach(i =>
{
    Console.Write("{0}\t", i);
});
//输出如下
//1       2       3       4       5       6
2. 迭代器的实现方式

有了我们自己的foreach,下一步就是实现我们自己的迭代器了。
C#的迭代器组成可能会让一些新人觉得非常复杂,但如上面实现的迭代函数所示,核心的成员其实非常少,如果对迭代器模式很熟悉,那就更简单了。
我们实现一个迭代器,这个迭代器通过输入两个整数作为范围(左开右闭),然后将这两个数之间所有的数全部进行输出。

  • 首先是IEnumerable部分,我这里继承了泛型接口,将我们的类命名为MyRange,在构造函数中传入两个个值表示范围;
  • 用于具体迭代的IEnumerator部分在将后面实现,将其命名为MyRangeIterator,同样继承了泛型接口。
  • 我在代码中使用了partical关键字,这个关键字表示类可以在多处进行编写,编译器会自动将这些类组合成一个,详细可以查看这个链接:partical参考,分开的目的是为了方便后面使用嵌套类来实现我们的MyRangeIterator
  • 内层的嵌套类可以直接使用外层的私有变量,详细参考可以查看这里:嵌套类参考

代码内容很简单,保存传入的两个值即可,具体代码如下:

	partial class MyRange : IEnumerable<int>
    {
        private int start, end;
        public MyRange(int start, int end)
        {
            this.start = start;
            this.end = end;
        }
        public IEnumerator<int> GetEnumerator()
        {
            return new MyRangeIterator(this);//待实现
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

然后实现我们的MyRangeIterator类型。

  • 由于我们要进行迭代,所以我声明了两个变量,cur表示当前迭代的值,end表示最后一个值。
  • Current用于获取当前迭代的值,直接返回cur即可。
  • MoveNext()用于判断迭代是否已经结束,当此函数返回false时,迭代器就迭代结束了。我们设计的类的功能是一个左闭右开的序列,所以当cur大于end时,我们的迭代就结束了,因此,在MoveNext中,使用++cur对迭代值进行自增,再将自增后的结果与end比较,返回cur < end的结果即可。
  • 因为我们没有特殊功能,所以DisposeReset都没有写具体实现。
	partial class MyRange
    {
        class MyRangeIterator : IEnumerator<int>
        {
            int end;
            int cur;
            public MyRangeIterator(MyRange parent)
            {
                this.end = parent.end;
                this.cur = parent.start;
            }
            public int Current => cur;

            object IEnumerator.Current => Current;

            public void Dispose() { }

            public bool MoveNext()
            {
                Console.WriteLine("MyRangeIterator MoveNext : " + cur);
                return ++cur <= end;
            }

            public void Reset() { }
        }

    }

然后进行测试测试:

var range = new MyRange(1,5);
Console.WriteLine("new range");
range.Foreach(i =>
{
    Console.WriteLine("print : " + i);
});
/*输出
new range
MyRangeIterator MoveNext : 1
print : 2
MyRangeIterator MoveNext : 2
print : 3
MyRangeIterator MoveNext : 3
print : 4
MyRangeIterator MoveNext : 4
print : 5
MyRangeIterator MoveNext : 5
*/

三、大大简化的迭代器——yield

从上面的代码可以看到,实现一个如此简单的迭代器就复杂到了这种程度,还不如直接for(int i = 0 ; i < 5 ; i++)来得简单快捷,一样的问题C#语言编写组也注意到了,于是他们祭出了大杀器——yield关键字。
yield关键字用于返回值为IEnumerableIEnumeratorIEnumerable<T>IEnumerator<T>函数中,它有两个使用方法,yield breakyield return

  • 返回值为IEnumerableIEnumeratorIEnumerable<T>IEnumerator<T>的函数会自动生成一个迭代器,每个yield语句都表示一次暂停或中断。
  • yield break用于中断当前迭代,此时,自动生成的迭代器的MoveNext()会返回false
  • yield return用于暂停当前函数,并返回一个值,返回的值可以在迭代器中的Current属性中获取。
  • 再次调用迭代器的MoveNext时,会自动从上一个yield return之后开始运行。

talk is cheap, show me the code。
下面我编写了一个函数,函数使用IEnumerable<int>作为返回值,依次返回1\2\3\4

IEnumerable<int> MyIter()
{
    int temp = 0;
    Console.WriteLine("before 1 , temp " + temp);
    yield return 1;
    temp = 1;
    Console.WriteLine("before 2");
    yield return 2;
    Console.WriteLine("before 3");
    yield return 3;
    Console.WriteLine("before break , temp " + temp);
    yield break;
    Console.WriteLine("after break");
    yield return 4;
}

/**************/
 foreach (var item in MyIter())
 {
     Console.WriteLine( "foreach " + item);
 }

输出:

before 1 , temp 0
foreach 1
before 2
foreach 2
before 3
foreach 3
before break , temp 1

这段代码有几个需要注意的地方:

  • 函数中的输出和foreach中的输出是依次进行的,说明每一个yield都使函数在当前位置暂停了运行。
  • 最后一行的"after break"没有输出,因为最后一行的yield return之前的yield break使迭代结束了,导致这一行没有被输出。
  • 在函数中定义了一个临时变量, 并在一次迭代后修改了这个值,几次迭代后输出,能够获得修改后的值,说明自动生成的迭代器可以保存值的修改结果。

四、使用yield简化MyRange迭代器

上面我们测试了yield的使用,如果用它来优化我们前面的迭代器,就可以获得更加简洁的代码,我们之前的MyRangeIterator类可以完全省略。

    class MyRange : IEnumerable<int>
    {
        private int start, end;
        public MyRange(int start, int end)
        {
            this.start = start;
            this.end = end;
        }
        public IEnumerator<int> GetEnumerator()
        {
            for (int i = start+1; i <= end; i++)//注意这里,直接使用一个循环替代了前面的迭代器
            {
                yield return i;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

还是一样的测试代码

var range = new MyRange(1,5);
Console.WriteLine("new range");
range.Foreach(i =>
{
    Console.WriteLine("print : " + i);
});
/*输出
new range
MyRangeIterator MoveNext : 1
print : 2
MyRangeIterator MoveNext : 2
print : 3
MyRangeIterator MoveNext : 3
print : 4
MyRangeIterator MoveNext : 4
print : 5
MyRangeIterator MoveNext : 5
*/

如果更极端一些,我们甚至可以仅使用一个迭代函数——

 IEnumerable<int> MyRange(int start,int end)
 {
     for (int i = start+1; i <= end; i++)
     {
         yield return i;
     }
 }
  var range = MyRange(1,5);
  Console.WriteLine("new range");
  range.Foreach(i =>
  {
      Console.WriteLine("print : " + i);
  });
/*输出
new range
MyRangeIterator MoveNext : 1
print : 2
MyRangeIterator MoveNext : 2
print : 3
MyRangeIterator MoveNext : 3
print : 4
MyRangeIterator MoveNext : 4
print : 5
MyRangeIterator MoveNext : 5
*/

五、结语

至此,C#原生迭代器的基本知识就告一段落了,后面我会继续更新两篇文章,分别展示迭代器的延迟调用以及基于此的LINQ的流式处理,和Unity基于迭代器实现的协程功能。

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值