.Net迭代器新思路
在通常的情况下,我们非常习惯于用foreach来迭代容器中的元素。虽然比起for循环来说,foreach可以让我们少打一些字母。
如果我们有下面的需求:把一个整数的容器中所有元素乘2,然后保存在新的容器中,那么我们通常会写下下面的代码:
2ArrayList src = new ArrayList(datas);
3ArrayList dest = new ArrayList();
4foreach (object obj in src)
5{
6 dest.Add(((int)obj) * 2);
7}
8
对于表达来说,我们真正需要注意的是“dest.Add(((int)obj) * 2);”这行代码。而在这行代码中,最重要的是“((int)obj) * 2”代码。而剩下的代码仅仅是为了辅助完成上面的任务。
真正在开发中,我们可能会经常遇到上面的命题。如果每次都来写foreach,然后是几行的辅助代码,确实感觉有点繁琐。那么我们有什么好的方法吗?能把foreach封装起来就好了。
在.Net的System.Collections.Generic命名空间里,我们会发现List类有下面几个方法:
ConvertAll 将当前 List 中的元素转换为另一种类型,并返回包含转换后的元素的列表。
Find 搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的第一个匹配元素。
FindAll 检索与指定谓词所定义的条件相匹配的所有元素。
FindLast 搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的最后一个匹配元素。
FindLastIndex 已重载。 搜索与指定谓词所定义的条件相匹配的元素,返回 List 或它的一部分中最后一个匹配项的从零开始的索引。
ForEach 对 List 的每个元素执行指定操作。
这些方法都有一个特殊的参数:代理。这些代理都是在System命名空间中声明的。
下面我们还是沿用刚才的命题,用ConvertAll来写一段代码完成任务。
2List<int> src = new List<int>(datas);
3List<int> dest = src.ConvertAll(delegate(int val)
4{
5 return val * 2;
6});
现在,你看不到foreach循环,而且也不用负责向dest中加入元素,你唯一需要关注的就是“val * 2”。现在都是.Net 3.5了,我们应该换上时髦的Lambda表达式!
2List<int> src = new List<int>(datas);
3List<int> dest = src.ConvertAll(val => val * 2);
现在让我们来数数,原本声明dest,foreach外加val * 2的3行代码,现在一行就完成了。而且从代码上你一眼就可以看明白“List<int> dest = src.ConvertAll(val => val * 2);”这句话是什么意思:声明一个dest容器,它应该是指向一个src以“val => val * 2”为规则转化而成的容器。
那么现在你肯定想知道这是怎么做到的。其实并不难,让我们来以ArrayList为基类创建一个自己的List类:MyList。
2{
3 public MyList() { }
4 public MyList(ICollection list) : base(list) { }
5 public MyList(int capacity) : base(capacity) { }
6}
好了,在此先创建一个泛型的MyList。继承自ArrayList。接下来,我们可以声明3个代理,负责3种不同的操作:ForEach、FindAll、CollectAll
2public delegate R CollectAllFunc<T, R>(T val);
3public delegate bool FindAllFunc<T>(T val);
下面我们就来创建ForEach方法、FindAll方法和CollectAll方法。首先,我们来看看ForEach。这个最简单。
2{
3 foreach (object obj in this)
4 {
5 act((T)obj);
6 }
7 return this;
8}
这类方法的设计思路大体是这样的。把变动的代码视为一个代码块,有输入和输出。这样就可以把变动的代码抽象成一个函数。于是我们就可以把变动的代码分析一下,得出需要接受多少参数,然后会返回什么结果。最后设计一个代理来限定这个代码块的大模样。而ForEach方法中,仅仅是固定不变的代码,如foreach循环。在最后之所以返回this,目的是为了链式调用:
list.ForEach(…).Add(…)
至于CollectAll和FindAll方法也使用同样的设计思路:
2{
3 MyList<RType> ret = new MyList<RType>();
4 foreach (object obj in this)
5 {
6 ret.Add(act((T)obj));
7 }
8 return ret;
9}
10
11public MyList<T> FindAll(FindAllFunc<T> act)
12{
13 MyList<T> ret = new MyList<T>();
14 foreach (object obj in this)
15 {
16 if(act((T)obj)) ret.Add(obj);
17 }
18 return ret;
19}
下面我们来看看这个MyList类的使用,当然,跟List类的使用基本一样。这次的需求增加了一下,需要先打印所有的元素,然后给每个元素×2,然后打印大于10的元素。
2MyList<int> list = new MyList<int>(datas);
3list.ForEach(val => Console.Write("{0}\t", val))
4 .CollectAll(val => val * 2).FindAll(val => val > 10)
5 .ForEach(val => Console.Write("{0}\t", val));
这段代码看上去都是一些方法调用,其实它替代了许多foreach循环。不过至于后面两个需求,使用一个foreach循环就可以办到,上面的链式调用对性能有所损失。不过依据个人的爱好吧,使用哪种方式都可以。
至于Lambda表达式,其实是脱胎于代理的一个概念(至少在.Net中可以这么说吧)。给代理赋值的时候,我们可以使用匿名函数的方式:
2{
3 //do something
4};
使用匿名函数在输入上会有些许麻烦,如果仅仅是一小段简单的代码,使用匿名函数可能会显得有点不值得。所以我们可以用Lambda表达式来达到同样的目的,但是可以少输入点字母:
唠叨了这么多,仅仅是对.Net迭代器的一些看法。也算是对在.Net中看到了Ruby式迭代器的一种兴奋。