本节即将新接触的CIL操作符如下:
br.s IL_003c 无条件地将控制转移到目标指令(短格式)
clt 从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中
ldloca.s CS$5$0001 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)
leave.s 退出受保护的代码区域,无条件将控制转移到目标指令(缩写形式)
constrained. 约束要对其进行虚方法调用的类型
endfinally 将控制从异常块的 fault 或 finally 子句转移回公共语言结构 (CLI) 异常处理程序
在C#中我们经常会遇到遍历数组、遍历List<>、遍历HashTable等情况,在本文中我们首先构造一个List<int>对象,然后通过For和Foreach来遍历它看看他们之间的CIL代码有什么区别和不同。
首先我们贴出C#代码如下:
class Program
{
static void Main( string [] args)
{
// 初始化一个List<int>
List < int > listInt = new List < int > ();
for ( int i = 0 ; i < 100000 ; i ++ )
{
listInt.Add(i + 1 );
}
Console.WriteLine( " -------------------------- " );
// 第一种for遍历
for ( int i = 0 ; i < listInt.Count; i ++ )
{
}
// 第二种foreach遍历
foreach ( int i in listInt)
{
}
}
}
其次我们来看CIL代码如下所示,因为CIL代码有点儿长,设置为隐藏有需要的可以点击查看:
.method private hidebysig static void Main( string [] args) cil managed
{
.entrypoint
// 代码大小 123 (0x7b)
.maxstack 3
.locals init ([ 0 ] class [mscorlib]System.Collections.Generic.List` 1 < int32 > listInt,
[ 1 ] int32 i,
[ 2 ] bool CS$ 4 $ 0000 ,
[ 3 ] valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 > CS$ 5 $ 0001 )
IL_0000: nop
IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List` 1 < int32 > ::.ctor()
IL_0006: stloc. 0
IL_0007: ldc.i4. 0
IL_0008: stloc. 1
IL_0009: br.s IL_001b
IL_000b: nop
IL_000c: ldloc. 0
IL_000d: ldloc. 1
IL_000e: ldc.i4. 1
IL_000f: add
IL_0010: callvirt instance void class [mscorlib]System.Collections.Generic.List` 1 < int32 > ::Add( ! 0 )
IL_0015: nop
IL_0016: nop
IL_0017: ldloc. 1
IL_0018: ldc.i4. 1
IL_0019: add
IL_001a: stloc. 1
IL_001b: ldloc. 1
IL_001c: ldc.i4 0x186a0
IL_0021: clt
IL_0023: stloc. 2
IL_0024: ldloc. 2
IL_0025: brtrue.s IL_000b
IL_0027: ldstr " -------------------------- "
IL_002c: call void [mscorlib]System.Console::WriteLine( string )
IL_0031: nop
IL_0032: ldc.i4. 0
IL_0033: stloc. 1
IL_0034: br.s IL_003c
IL_0036: nop
IL_0037: nop
IL_0038: ldloc. 1
IL_0039: ldc.i4. 1
IL_003a: add
IL_003b: stloc. 1
IL_003c: ldloc. 1
IL_003d: ldloc. 0
IL_003e: callvirt instance int32 class [mscorlib]System.Collections.Generic.List` 1 < int32 > ::get_Count()
IL_0043: clt
IL_0045: stloc. 2
IL_0046: ldloc. 2
IL_0047: brtrue.s IL_0036
IL_0049: nop
IL_004a: ldloc. 0
IL_004b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator <! 0 > class [mscorlib]System.Collections.Generic.List` 1 < int32 > ::GetEnumerator()
IL_0050: stloc. 3
. try
{
IL_0051: br.s IL_005d
IL_0053: ldloca.s CS$ 5 $ 0001
IL_0055: call instance ! 0 valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 > ::get_Current()
IL_005a: stloc. 1
IL_005b: nop
IL_005c: nop
IL_005d: ldloca.s CS$ 5 $ 0001
IL_005f: call instance bool valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 > ::MoveNext()
IL_0064: stloc. 2
IL_0065: ldloc. 2
IL_0066: brtrue.s IL_0053
IL_0068: leave.s IL_0079
} // end .try
finally
{
IL_006a: ldloca.s CS$ 5 $ 0001
IL_006c: constrained. valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 >
IL_0072: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0077: nop
IL_0078: endfinally
} // end handler
IL_0079: nop
IL_007a: ret
} // end of method Program::Main
// 第一种for遍历
IL_0031: nop
// 将整数值 0 作为 int32 推送到计算堆栈上
IL_0032: ldc.i4. 0
// 从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中
IL_0033: stloc. 1
// 无条件地将控制转移到目标指令(短格式)。
IL_0034: br.s IL_003c
IL_0036: nop
///////// //注意:这里就是循环内部需要处理的代码处,在本实例中无代码
IL_0037: nop
// 将索引 1 处的局部变量加载到计算堆栈上
IL_0038: ldloc. 1
// 将整数值 1 作为 int32 推送到计算堆栈上
IL_0039: ldc.i4. 1
// 将两个值相加并将结果推送到计算堆栈上。
IL_003a: add
// 从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中
IL_003b: stloc. 1
// 将索引 1 处的局部变量加载到计算堆栈上
IL_003c: ldloc. 1
// 将索引 0 处的局部变量加载到计算堆栈上
IL_003d: ldloc. 0
// 调用系统函数获取List数量
IL_003e: callvirt instance int32 class [mscorlib]System.Collections.Generic.List` 1 < int32 > ::get_Count()
// 比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
IL_0043: clt
// 从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中
IL_0045: stloc. 2
// 将索引 2 处的局部变量加载到计算堆栈上
IL_0046: ldloc. 2
// 如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。
IL_0047: brtrue.s IL_0036
// 第二种foreach遍历
IL_0049: nop
// 将索引 0 处的局部变量加载到计算堆栈上。
IL_004a: ldloc. 0
// 调用List<int> listInt对象的GetEnumerator()方法
// 任何集合类对象都有一个GetEnumerator()方法,该方法可以返回一个实现了 IEnumerator接口的对象,
// 这个返回的IEnumerator对象既不是集合类对象,也不是集合的元素类对象,它是一个独立的类对象。
// 通过这个对象,可以遍历访问集合类对象中的每一个元素对象 .
IL_004b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator <! 0 > class [mscorlib]System.Collections.Generic.List` 1 < int32 > ::GetEnumerator()
// 从计算堆栈的顶部弹出当前值并将其存储到索引 3 处的局部变量列表中
IL_0050: stloc. 3
. try
{
// 无条件地将控制转移到目标指令(短格式)。
IL_0051: br.s IL_005d
// 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
IL_0053: ldloca.s CS$ 5 $ 0001
// 调用get_Current()函数返回一个Object类型。
IL_0055: call instance ! 0 valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 > ::get_Current()
// 从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中
IL_005a: stloc. 1
IL_005b: nop
///////// //注意:这里就是循环内部需要处理的代码处,在本实例中无代码
IL_005c: nop
// 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
IL_005d: ldloca.s CS$ 5 $ 0001
// 调用MoveNext()函数运行到下一个元素
IL_005f: call instance bool valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 > ::MoveNext()
// 从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中
IL_0064: stloc. 2
// 将索引 2 处的局部变量加载到计算堆栈上
IL_0065: ldloc. 2
// 如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。
IL_0066: brtrue.s IL_0053
// 退出受保护的代码区域,无条件将控制转移到目标指令(缩写形式)。
IL_0068: leave.s IL_0079
} // end .try
finally
{
// 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
IL_006a: ldloca.s CS$ 5 $ 0001
// 约束要对其进行虚方法调用的类型
IL_006c: constrained. valuetype [mscorlib]System.Collections.Generic.List` 1 / Enumerator < int32 >
// 调用Dispose()将IEnumerator对象Dispose掉
IL_0072: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0077: nop
// 将控制从异常块的 fault 或 finally 子句转移回公共语言结构 (CLI) 异常处理程序
IL_0078: endfinally
} // end handler
从这里我们可以看出for方式的遍历是直接对元素集合本身的遍历,而foreach方式的遍历是对获取到元素集合的实现IEnumerator接口的对象,通过这个Current属性,调用MoveNext()函数对集合进行遍历的。
下面我们来看看for和foreach对List对象的不同数据量级别的访问时间如下,首先我们看耗时测算代码如下:
class Program
{
static void Main( string [] args)
{
// 初始化一个List<int>
List < int > listInt = new List < int > ();
for ( int i = 0 ; i < 1000000 ; i ++ )
{
listInt.Add(i + 1 );
}
Console.WriteLine( " -------------------------- " );
// 第一种for遍历
Stopwatch sw1 = new Stopwatch();
sw1.Start();
for ( int i = 0 ; i < listInt.Count; i ++ )
{
}
sw1.Stop();
Stopwatch sw2 = new Stopwatch();
sw2.Start();
// 第二种foreach遍历
foreach ( int i in listInt)
{
}
sw2.Stop();
Console.WriteLine( " 当前得List<int>对象数目: " + listInt.Count.ToString());
Console.WriteLine( @" for 的遍历消耗时间是: " + sw1.Elapsed);
Console.WriteLine( @" foreach 的遍历消耗时间是: " + sw2.Elapsed);
Console.ReadLine();
}
}
首先List<int> listInt为100的耗时如下三图:
其次List<int> listInt为10000的耗时如下三图:
最后我们看看List<int> listInt为10000的耗时如下三图:
结语:通过本篇文章的CIL我们知道了for和foreach在.NET环境的中间语言中是如何控制和循环的,另外也更加深入的了解for和foreach的区别。最后对于效率的比较可能和环境等有比较大的差异,大家可以不放可以自己建立一个控制台程序试试。