透过IL看C# (3)——foreach语句

透过IL看C# (3)
foreach语句

摘要:foreach语句是C#中一种重要的循环语句,用于遍历一个数组或对象集合中的每一个元素。这一篇文章介绍了在面对数组、IEnumerable接口和自定义类型时,编译器为foreach语句生成的IL代码。

foreach语句是C#中一种重要的循环语句,用于遍历一个数组或对象集合中的每一个元素。foreach语句的基本形式如下:

代码1 - foreach语句的基本形式

foreach([变量] in [集合]) [语句或语句块]

foreach语句的作用就是,对于代码1中的[集合],每次循环都取出一个元素放在[变量]中,然后执行一次[语句或语句块]。注意,在[语句或语句块]中,[变量]是只读的。也就是说,只能访问[变量]的值,而不能为其赋值。

在foreach语句中使用数组

数组是最简单的集合类型,也最常用在foreach语句中。代码2给出了一个简单的foreach循环。

代码2 - 使用数组的foreach循环

static void Test(int[] values) { foreach(int i in values) Console.WriteLine(i); }

代码3给出了使用ILDASM工具得到的编译器为上述代码生成的IL。其中我用C#代码给出了注释,便于查看。

代码3 - 代码2对应的IL

.method private hidebysig static void Test(int32[] values) cil managed { // 代码大小 34 (0x22) .maxstack 2 .locals init (int32 V_0, // i int32[] V_1, // values的副本 int32 V_2, // 中间变量,用做数组下标(循环变量) bool V_3) // 中间变量,用于判断循环终止条件 IL_0000: nop IL_0001: nop // V_1 = values IL_0002: ldarg.0 IL_0003: stloc.1 // V_2 = 0 IL_0004: ldc.i4.0 IL_0005: stloc.2 // goto IL_0017 IL_0006: br.s IL_0017 // V_0 = V_1[V_2] IL_0008: ldloc.1 IL_0009: ldloc.2 IL_000a: ldelem.i4 IL_000b: stloc.0 // Console.WriteLine(V_0) IL_000c: ldloc.0 IL_000d: call void [mscorlib]System.Console::WriteLine(int32) IL_0012: nop // V_2++ IL_0013: ldloc.2 IL_0014: ldc.i4.1 IL_0015: add IL_0016: stloc.2 // V_3 = V_2 < V_1.Length IL_0017: ldloc.2 IL_0018: ldloc.1 IL_0019: ldlen IL_001a: conv.i4 IL_001b: clt IL_001d: stloc.3 // if(V_3) goto IL_0008 IL_001e: ldloc.3 IL_001f: brtrue.s IL_0008 IL_0021: ret } // end of method Program::Test

代码3中出现的中间变量V_2(在代码2中没有对应的变量)和IL_0013处的“V_2++”暴露了这段代码的结构特征——和for语句是一样的。

由此,可以得到第一个结论——对于使用数组的foreach语句来说,编译器会将其翻译为和for语句类似的IL代码。但要注意,两者之间还是有区别的,前文已经提到过,在foreach语句中数组元素是只读的。

在foreach语句中使用一般集合(ICollection)

接下来,我们尝试在foreach循环中使用一般性的集合,这里用到的是一个实现了ICollection<int>接口的参数。请参见代码4。

代码4 - 使用ICollection的foreach循环

static void Test(ICollection<int> values) { foreach(int i in values) Console.WriteLine(i); }

代码5是代码4对应的IL代码,同样给出了C#语句的注释。

代码5 - 代码4对应的IL

.method private hidebysig static void Test(class [mscorlib]System.Collections.Generic.ICollection`1<int32> values) cil managed { // 代码大小 55 (0x37) .maxstack 2 .locals init (int32 V_0, // i class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> V_1, bool V_2) IL_0000: nop IL_0001: nop // V_1 = (IEnumerator<int>)values.GetEnumerator() IL_0002: ldarg.0 IL_0003: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() IL_0008: stloc.1 .try { // goto IL_0019 IL_0009: br.s IL_0019 // V_0 = V_1.Current IL_000b: ldloc.1 IL_000c: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() IL_0011: stloc.0 // Console.WriteLine(V_0) IL_0012: ldloc.0 IL_0013: call void [mscorlib]System.Console::WriteLine(int32) IL_0018: nop // V_2 = V_1.MoveNext() IL_0019: ldloc.1 IL_001a: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_001f: stloc.2 // if(V_2) goto IL_000b else goto IL_0035 //(IL_0035===ret) IL_0020: ldloc.2 IL_0021: brtrue.s IL_000b IL_0023: leave.s IL_0035 } // end .try finally { // if(V_1 != null) V_1.Dispose() IL_0025: ldloc.1 IL_0026: ldnull IL_0027: ceq IL_0029: stloc.2 IL_002a: ldloc.2 IL_002b: brtrue.s IL_0034 IL_002d: ldloc.1 IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0033: nop IL_0034: endfinally } // end handler IL_0035: nop IL_0036: ret } // end of method Program::Test

可以看到,在方法的最开始,即调用ICollection类型的GetEnumerable()方法得到了一个枚举器(IEnumerator)。

从IL_0009到IL_0023(.try块内的部分)实际上是一个while循环结构。只不过在用C#写程序时,是在while语句顶部进行条件判断的,而对应的IL则是在整个循环的底部进行判断。

由此,我们又得到了第二个结论——对于一般性的集合,foreach语句会首先会将集合转变为IEnumerable接口(ICollection接口继承自IEnumerable),然后取得IEnumerator对象,之后通过while循环进行遍历。

同时,也纠正了前文的一个说法,那就是,foreach语句不仅适用于“集合”,只要是“可枚举”的对象(实现了IEnumerable接口),都可以使用foreach进行遍历。

在foreach语句中使用自定义类型

让我们再进一步,如果我们自己写一个类型,能否将其对象作为“集合”用foreach进行遍历呢?

通过上面的结论2可以推测,只要我们自己写的类型实现了IEnumerable接口(包括IEnumerable泛型接口),就应该可以。

答案也是肯定的。

不过,事实是,即便不实现IEnumerable接口,只要提供了签名是public IEnumerator GetEnumerator()的方法,一个自定义类型的对象依然可以通过foreach进行遍历。例如代码6给出的类型。

代码6 - 可以在foreach语句中使用的自定义类型

public class MyEnumerator { public IEnumerator<int> GetEnumerator() { for (var i = 0; i < 10; i++) yield return i; } }

需要注意的是,这个GetEnumerator方法,必须是public并且不带参数,其返回值必须是IEnumerator或IEnumerator<T>。这个限制,其实和实现IEnumerable接口是一样的。所以,大家只要了解到这一特性就可以了,如果真的需要使用foreach语句遍历自己的类型,还是实现IEnumerable接口为上。

下面是一个使用自定义类型的foreach语句的示例。编译器为其生成的IL与代码5类似,这里就不再罗列了,读者可以自己用ILDasm看一看。

代码7 - 使用自定义类型的foreach语句

static void Test(MyEnumerator values) { foreach(int i in values) Console.WriteLine(i); }

小结

本文介绍了在foreach语句中使用数组、一般性集合和自定义类型的情形。

在使用数组时,foreach和for是等价的(但foreach只能进行只读的遍历),所以我们无需担心获取和使用枚举器时的开销。

在使用一般集合和自定义类型时,实际上是通过调用GetEnumerator方法获取了枚举器之后,通过while循环进行遍历的。虽然一个自定义类型只要实现了具有特定签名的GetEnumerator方法,就可以结合foreach语句使用,但还是建议在开发这样的类型时实现IEnumerable接口。

返回目录:透过IL看C#

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值