编写高质量代码改善C#程序的157个建议——建议17:多数情况下使用foreach进行循环遍历
建议17:多数情况下使用foreach进行循环遍历
由于本建议涉及集合的遍历,所以在开始讲解本建议之前,我们不妨来设想一下如何对结合进行遍历。假设存在一个数组,其遍历模式可以采用依据索引来进行遍历的方法;又假设存在一个HashTable,其遍历模式可能是按照键值来进行遍历。无论是哪个集合,如果他们的遍历没有一个公共的接口,那么客户端在进行遍历时,相当于是对具体类型进行了编码。这样一来,当需求发生变化时,必须修改我们的代码。而且,由于客户端代码过多地关注了集合内部的实现,代码的可移植性就会变得很差,这直接违反了面向对象的开闭原则。于是,迭代器模式就诞生了。现在,不要管FCL中如何实现该模式的,我们先来实现一个自己的迭代器模式。
/// <summary> /// 要求所有的迭代器全部实现该接口 /// </summary> interface IMyEnumerator { bool MoveNext(); object Current { get; } } /// <summary> /// 要求所有的集合实现该接口 /// 这样一来,客户端就可以针对该接口编码, /// 而无须关注具体的实现 /// </summary> interface IMyEnumerable { IMyEnumerator GetEnumerator(); int Count { get; } } class MyList : IMyEnumerable { object[] items = new object[10]; IMyEnumerator myEnumerator; public object this[int i] { get { return items[i]; } set { this.items[i] = value; } } public int Count { get { return items.Length; } } public IMyEnumerator GetEnumerator() { if (myEnumerator == null) { myEnumerator = new MyEnumerator(this); } return myEnumerator; } } class MyEnumerator : IMyEnumerator { int index = 0; MyList myList; public MyEnumerator(MyList myList) { this.myList = myList; } public bool MoveNext() { if (index + 1 > myList.Count) { index = 1; return false; } else { index++; return true; } } public object Current { get { return myList[index - 1]; } } }
static void Main(string[] args) { //使用接口IMyEnumerable代替MyList IMyEnumerable list = new MyList(); //得到迭代器,在循环中针对迭代器编码,而不是集合MyList IMyEnumerator enumerator = list.GetEnumerator(); for (int i = 0; i < list.Count; i++) { object current = enumerator.Current; enumerator.MoveNext(); } while (enumerator.MoveNext()) { object current = enumerator.Current; } }
MyList模拟了一个集合类,它继承了接口IMyEnumerable,这样,在客户端调用的时候,我们就可以直接调用IMyEnumerable来声明变量,如代码中的一下语句:
IMyEnumerable list=new MyList();
如果未来我们新增了其他的集合类,那么针对list的编码即使不做修改也能运行良好。在IMyEnumerable中声明了GetEnumerator方法返回一个继承了IMyEnumerator的对象。在MyList的内部,默认返回MyEnumerator,MyEnumerator就是迭代器的一个实现,如果对于迭代的需求有变化,可以重新开发一个迭代器(如下所示),然后在客户端迭代的时候使用该迭代器。
//使用接口IMyEnumerable代替MyList IMyEnumerable list = new MyList(); //得到迭代器,在循环中针对迭代器编码,而不是集合MyList IMyEnumerator enumerator2 = new MyEnumerator(list);
//for调用 for (int i = 0; i < list.Count; i++) { object current = enumerator2.Current; enumerator.MoveNext(); }
//while调用 while (enumerator.MoveNext()) { object current = enumerator2.Current; }
在客户端的代码中,我们在迭代的过程中分别演示了for循环和while循环,到那时因为使用了迭代器的缘故,两个循环都没有针对MyList编码,而是实现了对迭代器的编码。
理解了自己实现的迭代器模式,相当于理解了FCL中提供的对应模式。以上代码中,在接口和类型中都加入了“My”字样,其实,FCL中有与之相对应的接口和类型,只不过为了演示需要,增加了其中部分内容,但是大致思路是一样的。使用FCL中相应类型进行客户端代码编写,大致应该下面这样:
ICollection<object> list = new List<object>(); IEnumerator enumerator = list.GetEnumerator();
for (int i = 0; i < list.Count; i++) { object current = enumerator.Current; enumerator.MoveNext(); }
while (enumerator.MoveNext()) { object current = enumerator.Current; }
但是,无论是for循环还是while循环,都有些啰嗦,于是,foreach就出现了。
foreach (var current in list) { //省略了 object current = enumerator.Current; }
可以看到,采用foreach最大限度地简化了代码。它用于遍历一个继承了IEnumerable或IEnumerable<T>接口的集合元素。借助IL代码,我们查看使用foreach到底发生了什么事情:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 62 (0x3e) .maxstack 2 .locals init ([0] class [mscorlib]System.Collections.Generic.ICollection`1<object> list, [1] object current, [2] class [mscorlib]System.Collections.Generic.IEnumerator`1<object> CS$5$0000, [3] bool CS$4$0001) IL_0000: nop IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<object>::.ctor() IL_0006: stloc.0 IL_0007: nop IL_0008: ldloc.0 IL_0009: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<object>::GetEnumerator() IL_000e: stloc.2 .try { IL_000f: br.s IL_001a IL_0011: ldloc.2 IL_0012: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<object>::get_Current() IL_0017: stloc.1 IL_0018: nop IL_0019: nop IL_001a: ldloc.2 IL_001b: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_0020: stloc.3 IL_0021: ldloc.3 IL_0022: brtrue.s IL_0011 IL_0024: leave.s IL_0036 } // end .try finally { IL_0026: ldloc.2 IL_0027: ldnull IL_0028: ceq IL_002a: stloc.3 IL_002b: ldloc.3 IL_002c: brtrue.s IL_0035 IL_002e: ldloc.2 IL_002f: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0034: nop IL_0035: endfinally } // end handler IL_0036: nop IL_0037: call int32 [mscorlib]System.Console::Read() IL_003c: pop IL_003d: ret } // end of method Program::Main
查看IL代码就可以看出,运行时还是会调用get_Current()和MoveNext()方法。
在调用完MoveNext()方法后,如果结果是true,跳转到循环开始处。实际上foreach循环和while循环是一样的:
while (enumerator.MoveNext()) { object current = enumerator.Current; }
foreach循环除了可以提供简化的语法外,还有另外两个优势:
- 自动将代码置入try finally块
- 若类型实现了IDisposable接口,它会在循环结束后自动调用Dispose方法。
转自:《编写高质量代码改善C#程序的157个建议》陆敏技