本篇文章主要展示C#迭代器的实现和yield关键字的使用。
在熟悉了这些C#迭代器的基础之后,我会在后面两篇博客中展示迭代器的延迟处理和LINQ中流处理的实现,以及Unity中的协程的实现,如果你已经对这部分内容熟悉了,可以略过本篇,直接进入后面两篇文章。
C#迭代器的实现和应用(二)——延迟执行、流式处理与两个基本LINQ扩展的实现
C#迭代器的实现和应用(三)——Unity的协程分析以及实现自己的协程
设计模式是现在所有程序员在进阶时都应该学习的知识,其中的迭代器模式是一个很不起眼但又被广泛使用的模式,有很多语言都原生支持了这种模式,包括C#、C++、JAVA、Lua等等等等。
迭代器模式提供了不需要考虑集合的实际内部结构条件下对集合中对象进行遍历的能力,我以前也实现过lua的泛型迭代器:《Lua的for泛型迭代器使用方式》。
关于迭代器模式的详细介绍和实现可以查看这里:《迭代器模式|菜鸟教程》。菜鸟教程里的代码是JAVA中的实现,在C#中的实现又有所区别,并且由于语法原生的支持和优化,C#中的迭代器可用性变得更强,其他的应用也变得非常强大。
九层之台,起于垒土,我们还是从最基础的内容开始,在C#中实现一个最基础的、没有语法糖优化的迭代器。
与前面设计模式的文章一样,在C#中,迭代操作也是由两部分组成——迭代器和迭代函数,多数情况下,C#中使用foreach关键字来对迭代器进行迭代,因此,为了拆解内部实现,我们有必要也实现一个C#的迭代函数。
在C#中自带几个迭代器接口,其使用与设计模式中所说的完全一致,类图如下:
简单来说迭代器分为两部分:
- 集合要实现IEnuerable接口,实现GetEnumerator方法,这个成员方法会返回一个IEnumerator对象,这个对象就是我们的迭代器;
- 迭代器实现IEnumerator接口,这个接口只有三个方法:
MoveNext
方法用于判断是否已经到达了迭代的终点,Current
属性用于获取当前迭代的值,Reset
方法用于重置迭代器。- 此外,这两个类分别还有对应的泛型类。
IEnumerator
为了实现对特定对象的释放,又额外继承了IDisposable
接口。
光有了迭代器也不行,我们当然还需要一个自己的迭代函数,在C#中原生提供了foreach
作为关键字,以foreach(Type value in IEnumerable)
的形式进行迭代,集合中每一个值都会被转换成Type
,通过value
直接获取即可。
注意!如果使用的是非泛型的对象,这里会使用强制转换,类型不安全,并且如果集合中的对象是值类型,也会发生一个拆箱操作,造成额外的性能问题。
foreach
的实现其实很简单,如设计模式中所示,对集合调用GetEnumerator
方法获取迭代器,然后使用while循环,直到迭代器的MoveNext
方法返回false,当MoveNext
方法返回的值不为false时,可以对迭代器调用Current
属性,获取迭代器当前所对应的值。
实现代码如下,为了实现对数据的操作,我传入了一个Action
作为回调用于处理。
注意!这里我仅仅实现了相同的迭代行为,实际使用的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
有了我们自己的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
的结果即可。Dispose
和Reset
都没有写具体实现。 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
关键字用于返回值为IEnumerable
、IEnumerator
、IEnumerable
、IEnumerator
函数中,它有两个使用方法,yield break
和yield return
。
IEnumerable
、IEnumerator
、IEnumerable
、IEnumerator
的函数会自动生成一个迭代器,每个yield
语句都表示一次暂停或中断。yield break
用于中断当前迭代,此时,自动生成的迭代器的MoveNext()
会返回false
。yield return
用于暂停当前函数,并返回一个值,返回的值可以在迭代器中的Current
属性中获取。MoveNext
时,会自动从上一个yield return
之后开始运行。talk is cheap, show me the code。
下面我编写了一个函数,函数使用IEnumerable
作为返回值,依次返回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
这段代码有几个需要注意的地方:
yield
都使函数在当前位置暂停了运行。yield return
之前的yield break
使迭代结束了,导致这一行没有被输出。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基于迭代器实现的协程功能。