C#特性-迭代器(上)及一些研究过程中的副产品
作者:mdxy-dxy 时间:2023-12-05 18:26:49
提到迭代器我们不能不想到迭代器模式,那我就以迭代器模式作为开场白.
在我们的应用程序中常常有这样一些数据结构:
它们是一个数据的集合,如果你知道它们内部的实现结构就可以去访问它们,它们各自的内部存储结构互不相同,各种集合有各自的应用场合.说到这里大家可能想出一大堆这样的集合了:List,Hashtable,ArrayList等等。这些集合各自都有各自的个性,这就是它们存在的理由。但如果你想遍历它你必须知道它内部的存储细节,作为一个集合元素,把内部细节暴露出来肯定就不好了,这样客户程序就不够稳定了,在你更换集合对象的时候,比如List不能满足需求的时候,你换成Hashtable,因为以前的客户程序过多的关注了List内部实现的细节,所以不能很好的移植。而迭代器模式就是为解决这个问题而生的:
提供一种一致的方式访问集合对象中的元素,而无需暴露集合对象的内部表示。
比如现在有这样一个需求,遍历集合内的元素,然后输出,但是并不限定集合是什么类型的集合,也就是未来集合可能发生改变。
思考:
集合会发生改变,这是变化点,集合改变了,遍历方法也改变,我们要保证遍历的方法稳定,那么就要屏蔽掉细节。找到了变化点那我们就将其隔离起来(一般使用interface作为隔离手段):假设所有的集合都继承自ICollection接口,这个接口用来隔离具体集合的,将集合屏蔽在接口后面,作为遍历我们肯定需要这样一些方法:MoveNext,Current,既然ICollection负责数据存储,职责又要单一,那么就新建立一个接口叫做Iterator吧,每种具体的集合都有自己相对应的Iterator实现:
下面是一个简易的实现代码:
/// <summary>
/// 集合的接口
/// </summary>
public interface ICollection
{
int Count { get; }
/// <summary>
/// 获取迭代器
/// </summary>
/// <returns>迭代器</returns>
Iterator GetIterator();
}
/// <summary>
/// 迭代器接口
/// </summary>
public interface Iterator
{
bool MoveNext();
object Current { get; }
}
public class List : ICollection
{
private const int MAX = 10;
private object[] items;
public List()
{
items = new object[MAX];
}
public object this[int i]
{
get { return items[i]; }
set { this.items[i] = value; }
}
#region ICollection Members
public int Count
{
get { return items.Length; }
}
public Iterator GetIterator()
{
return new ListIterator(this);
}
#endregion
}
public class ListIterator : Iterator
{
private int index = 0;
private ICollection list;
public ListIterator(ICollection list)
{
this.list = list;
index = 0;
}
#region Iterator Members
public bool MoveNext()
{
if (index + 1 > list.Count)
return false;
else
{
index++;
return true;
}
}
public object Current
{
get { return list[index]; }
}
#endregion
}
/// <summary>
/// 测试
/// </summary>
public class Program
{
static void Main()
{
ICollection list = new List();
Iterator iterator = list.GetIterator();
while (iterator.MoveNext())
{
object current = iterator.Current;
}
}
}
看看最后的测试,是不是不管具体的集合如何改变,遍历代码都非常稳定?而且扩展新的集合类也非常方便,只是添加代码不会修改原来的代码,符合开闭原则。当然,这么好的解决方案微软当然不会放过,现在C# 2.0里已经内置了对迭代器的支持,看看System.Collections, System.Collections.Generic命名空间,所有的集合都实现了这个接口:IEnumerable,这个接口还有泛型的版本。注意到这个接口只有一个方法:IEnumerator GetEnumerator();,IEnumerator就是迭代器的接口,相当于我的实例里面的Iterator,它也有泛型的版本。
那么现在在.net里所有的集合类都可以这样访问了:
IEnumerator ienumerator = list.GetEnumerator();
while(ienumerator.MoveNext())
{
object current = ienumerator.Current;
}
但是这样访问也太麻烦了,所以C#里出现了foreach关键字,我们来看看foreach背后发生了什么?假如有如下的代码:
public static void Main()
{
ArrayList list = new ArrayList();
list.Add(1);
list.Add(2);
list.Add(3);
foreach (object item in list)
{
Console.WriteLine(item.ToString());
}
}
下面是它对应的IL代码:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 2
.locals init (
[0] class [mscorlib]System.Collections.ArrayList list,
[1] object item,
[2] class [mscorlib]System.Collections.IEnumerator CS$5$0000,
[3] class [mscorlib]System.IDisposable CS$0$0001)
L_0000: newobj instance void [mscorlib]System.Collections.ArrayList::.ctor()
L_0005: stloc.0
L_0006: ldloc.0
L_0007: ldc.i4.1
L_0008: box int32
L_000d: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
L_0012: pop
L_0013: ldloc.0
L_0014: ldc.i4.2
L_0015: box int32
L_001a: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
L_001f: pop
L_0020: ldloc.0
L_0021: ldc.i4.3
L_0022: box int32
L_0027: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
L_002c: pop
L_002d: ldloc.0
L_002e: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Collections.ArrayList::GetEnumerator()
L_0033: stloc.2
L_0034: br.s L_0048
L_0036: ldloc.2
L_0037: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
L_003c: stloc.1
L_003d: ldloc.1
L_003e: callvirt instance string [mscorlib]System.Object::ToString()
L_0043: call void [mscorlib]System.Console::WriteLine(string)
L_0048: ldloc.2
L_0049: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
L_004e: brtrue.s L_0036
L_0050: leave.s L_0063
L_0052: ldloc.2
L_0053: isinst [mscorlib]System.IDisposable
L_0058: stloc.3
L_0059: ldloc.3
L_005a: brfalse.s L_0062
L_005c: ldloc.3
L_005d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0062: endfinally
L_0063: call string [mscorlib]System.Console::ReadLine()
L_0068: pop
L_0069: ret
.try L_0034 to L_0052 finally handler L_0052 to L_0063
}
从.locals init 那里可以看出编译器为我们添加了两个局部变量,一个就是迭代器。
L_002d: ldloc.0
L_002e: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Collections.ArrayList::GetEnumerator()
L_0033: stloc.2
这三行代码告诉我们,调用list的GetEnumerator()方法,获取迭代器实例将其赋值给编译器为我们添加的那个迭代器局部变量,接着是L_0034: br.s L_0048,
br.s这个指令是强制跳转,我们接着看
L_0048: ldloc.2
L_0049: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
调用迭代器的MoveNext()方法,L_004e: brtrue.s L_0036 如果是true的话跳转,
L_0036: ldloc.2
L_0037: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
L_003c: stloc.1
L_003d: ldloc.1
L_003e: callvirt instance string [mscorlib]System.Object::ToString()
L_0043: call void [mscorlib]System.Console::WriteLine(string)
获取当前值,然后输出
看到没有,实际foreach后面干的事就是获取迭代器,然后一个while循环,不过这样一些确实简洁多了。
说到这里是不是
IEnumerator ienumerator = list.GetEnumerator();
while (ienumerator.MoveNext())
{
object item = ienumerator.Current;
Console.WriteLine(item.ToString());
}
和
foreach (object item in list)
{
Console.WriteLine(item.ToString());
}
这两样代码是一样的呢?如果不一样那推荐使用哪一个呢?当然是使用第二种,简洁嘛,除了简洁之外就没有其它的了?细心读者会发现上面的IL代码,
在结束循环后还有一大块,可我们的C#代码中并没有啊,接着分析:
.try L_0034 to L_0052 finally handler L_0052 to L_0063
这里说明从L_0034到L_0052是被放在try里面的,恰好这段代码是循环体里的东西,L_0052到L_0063里是放在finally里的,看来foreach还给我们加了一
个try{}finally{}结构啊。那看看L_0052到L_0063里是什么东西吧:
L_0052: ldloc.2
L_0053: isinst [mscorlib]System.IDisposable
L_0058: stloc.3
L_0059: ldloc.3
L_005a: brfalse.s L_0062
L_005c: ldloc.3
L_005d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0062: endfinally
L_0063: call string [mscorlib]System.Console::ReadLine()
判断迭代器对象是否是一个IDisposable实例,如果是,那就要调用它的Dispose()方法了(为啥它要实现IDisposable接口?那肯定这个迭代器里使用了一些非托管资源)。
看到了吧,foreach也真够智能的,看来使用foreach的方式是比自己用while方式安全稳定多了。
(PS:好像是扯远了点,不过大家一起了解一下,呵呵,其实我当初也没想说这个,不过后来看IL代码有点不对劲,就当作个副产品吧)
C# 2.0里还出现个关键字yield,我看了半天MSDN也没明白它的意思:
在迭代器块中用于向枚举数对象提供值或发出迭代结束信号。到现在还是没明白,不过yield这个东西后面却包含了很多东西,有一些非常“奇怪”的特性,
我称之为奇怪的意思是与我们以前的思维有的不符,Linq的一些特质也是建立在这个特性之上的。关于yield的更多讨论我想放在另外一篇文章中,因为我觉得有必要。