.net: 不能忽视的break——寻找VS2010和VS2012编译器的一个小区别
文中的问题来自于实际开发,但是实际开发中的代码逻辑比较复杂,因此下面的代码去掉了所有逻辑,只保留能体现问题的代码,类和都只为了说明问题,并不具有实际意义。下面首先看看下面的代码和现象。
1. 问题再现
下面的代码重现了场景, 看完这段代码是不有任何问题吗?下面看看输出结果。
1 public class IL 2 { 3 public List<InstanceOne> _instances = new List<InstanceOne>(); 4 public InstanceOne _curInstance; 5 public Action CallBack; 6 7 public IL() 8 {//创建一个用来模拟的List对象 9 for (int i = 0; i < 5; i++) 10 { 11 _instances.Add(new InstanceOne() { Index = i, Property1 = "Property" + i }); 12 } 13 14 } 15 16 public void Test() 17 { 18 foreach (var item in _instances) 19 { 20 if (item.Index == 1) 21 { 22 Console.WriteLine("Before callback Item index is " + item.Index); 23 Thread thread = new Thread(CallBackInvoke); 24 this.CallBack += () => 25 { 26 Console.WriteLine("Current Item index is " + item.Index); 27 }; 28 thread.Start(); 29 } 30 } 31 } 32 33 public void CallBackInvoke() 34 { 35 Thread.Sleep(1000); 36 if (this.CallBack != null) 37 this.CallBack(); 38 } 39 } 40 41 public class InstanceOne 42 { 43 public int Index { get; set; } 44 public string Property1 { get; set; } 45 public string Property2 { get; set; } 46 }
下面是测试调用代码:
1 static void Main(string[] args) 2 { 3 IL il = new IL(); 4 il.Test(); 5 Console.ReadKey(); 6 }
执行上面的代码之后看看2012的输出结果:
我们在Callback回调中使用的对象还是Index等于1的对象,执行结果并没有什么不妥。但是大家不要以为这样就万事大吉了。我们的代码是在VS2012中开发的,但是别人没有VS2012,只有VS2010,所以代码需要在VS2010中编译和调试。但是当代码在VS2010中调试时很奇怪的一幕发生了!!为什么,看看下面的输出结果。
我们在Callback回调里拿到的对象不是我们想象的Index等于1.而是等于4的对象。同样的代码为啥呢?同样的代码,不同的结果,可能程序员第一反应都是我的代码没变,就是没问题。好吧,无论如何,这依然是需要我们去深究的问题。这样来证明我们的代码是没问题还是有问题。
注:上面的问题是通过debug发现使用的对象并不是想要的index==1,而是index==4. 这个代码中的输出只为体现对象不同而输出的。
2. 代码分析
代码相同,但是结果却大相近庭,这促使我们不得不一探究竟。但是到底是为什么呢?嗯。。。?对,他们是VS2010和VS2012的编译的不同版本,会不会是他们不同导致的结果?那怎么判断是不是VS不同版本之间的问题呢?对,想到了IL反编译,下面列出VS2010和VS2012的对方法Test()的反编译结果,然后我们来一起看看他们到底在编译时编译器做了什么导致他们会有不同的结果(关于如何反编译,可以使用微软自带的工具ildasm.exe)。
VS 2010的IL 反编译结果:
1 .method public hidebysig instance void Test() cil managed 2 { 3 // Code size 172 (0xac) 4 .maxstack 5 5 .locals init ([0] class [mscorlib]System.Threading.Thread thread, 6 [1] class [mscorlib]System.Action 'CS$<>9__CachedAnonymousMethodDelegate2', 7 [2] class ILResearch.IL/'<>c__DisplayClass3' 'CS$<>8__locals4', 8 [3] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne> CS$5$0000) 9 IL_0000: ldarg.0 10 IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<class ILResearch.InstanceOne> ILResearch.IL::_instances 11 IL_0006: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class ILResearch.InstanceOne>::GetEnumerator() 12 IL_000b: stloc.3 13 .try 14 { 15 IL_000c: ldnull 16 IL_000d: stloc.1 17 IL_000e: newobj instance void ILResearch.IL/'<>c__DisplayClass3'::.ctor() 18 IL_0013: stloc.2 19 IL_0014: br.s IL_008f 20 IL_0016: ldloc.2 21 IL_0017: ldloca.s CS$5$0000 22 IL_0019: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::get_Current() 23 IL_001e: stfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item 24 IL_0023: ldloc.2 25 IL_0024: ldfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item 26 IL_0029: callvirt instance int32 ILResearch.InstanceOne::get_Index() 27 IL_002e: ldc.i4.1 28 IL_002f: bne.un.s IL_008f 29 IL_0031: ldstr "Before callback Item index is " 30 IL_0036: ldloc.2 31 IL_0037: ldfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item 32 IL_003c: callvirt instance int32 ILResearch.InstanceOne::get_Index() 33 IL_0041: box [mscorlib]System.Int32 34 IL_0046: call string [mscorlib]System.String::Concat(object, 35 object) 36 IL_004b: call void [mscorlib]System.Console::WriteLine(string) 37 IL_0050: ldarg.0 38 IL_0051: ldftn instance void ILResearch.IL::CallBackInvoke() 39 IL_0057: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, 40 native int) 41 IL_005c: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart) 42 IL_0061: stloc.0 43 IL_0062: ldarg.0 44 IL_0063: dup 45 IL_0064: ldfld class [mscorlib]System.Action ILResearch.IL::CallBack 46 IL_0069: ldloc.1 47 IL_006a: brtrue.s IL_0079 48 IL_006c: ldloc.2 49 IL_006d: ldftn instance void ILResearch.IL/'<>c__DisplayClass3'::'<Test>b__1'() 50 IL_0073: newobj instance void [mscorlib]System.Action::.ctor(object, 51 native int) 52 IL_0078: stloc.1 53 IL_0079: ldloc.1 54 IL_007a: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, 55 class [mscorlib]System.Delegate) 56 IL_007f: castclass [mscorlib]System.Action 57 IL_0084: stfld class [mscorlib]System.Action ILResearch.IL::CallBack 58 IL_0089: ldloc.0 59 IL_008a: callvirt instance void [mscorlib]System.Threading.Thread::Start() 60 IL_008f: ldloca.s CS$5$0000 61 IL_0091: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::MoveNext() 62 IL_0096: brtrue IL_0016 63 IL_009b: leave.s IL_00ab 64 } // end .try 65 finally 66 { 67 IL_009d: ldloca.s CS$5$0000 68 IL_009f: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne> 69 IL_00a5: callvirt instance void [mscorlib]System.IDisposable::Dispose() 70 IL_00aa: endfinally 71 } // end handler 72 IL_00ab: ret 73 } // end of method IL::Test
VS 2012的IL 反编译结果:
1 .method public hidebysig instance void Test() cil managed 2 { 3 // Code size 175 (0xaf) 4 .maxstack 4 5 .locals init ([0] class [mscorlib]System.Threading.Thread thread, 6 [1] class [mscorlib]System.Action 'CS$<>9__CachedAnonymousMethodDelegate2', 7 [2] class ILResearch.IL/'<>c__DisplayClass3' 'CS$<>8__locals4', 8 [3] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne> CS$5$0000) 9 IL_0000: ldarg.0 10 IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<class ILResearch.InstanceOne> ILResearch.IL::_instances 11 IL_0006: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class ILResearch.InstanceOne>::GetEnumerator() 12 IL_000b: stloc.3 13 .try 14 { 15 IL_000c: br IL_0092 16 IL_0011: ldnull 17 IL_0012: stloc.1 18 IL_0013: newobj instance void ILResearch.IL/'<>c__DisplayClass3'::.ctor() 19 IL_0018: stloc.2 20 IL_0019: ldloc.2 21 IL_001a: ldloca.s CS$5$0000 22 IL_001c: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::get_Current() 23 IL_0021: stfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item 24 IL_0026: ldloc.2 25 IL_0027: ldfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item 26 IL_002c: callvirt instance int32 ILResearch.InstanceOne::get_Index() 27 IL_0031: ldc.i4.1 28 IL_0032: bne.un.s IL_0092 29 IL_0034: ldstr "Before callback Item index is " 30 IL_0039: ldloc.2 31 IL_003a: ldfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item 32 IL_003f: callvirt instance int32 ILResearch.InstanceOne::get_Index() 33 IL_0044: box [mscorlib]System.Int32 34 IL_0049: call string [mscorlib]System.String::Concat(object, 35 object) 36 IL_004e: call void [mscorlib]System.Console::WriteLine(string) 37 IL_0053: ldarg.0 38 IL_0054: ldftn instance void ILResearch.IL::CallBackInvoke() 39 IL_005a: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, 40 native int) 41 IL_005f: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart) 42 IL_0064: stloc.0 43 IL_0065: ldarg.0 44 IL_0066: dup 45 IL_0067: ldfld class [mscorlib]System.Action ILResearch.IL::CallBack 46 IL_006c: ldloc.1 47 IL_006d: brtrue.s IL_007c 48 IL_006f: ldloc.2 49 IL_0070: ldftn instance void ILResearch.IL/'<>c__DisplayClass3'::'<Test>b__1'() 50 IL_0076: newobj instance void [mscorlib]System.Action::.ctor(object, 51 native int) 52 IL_007b: stloc.1 53 IL_007c: ldloc.1 54 IL_007d: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, 55 class [mscorlib]System.Delegate) 56 IL_0082: castclass [mscorlib]System.Action 57 IL_0087: stfld class [mscorlib]System.Action ILResearch.IL::CallBack 58 IL_008c: ldloc.0 59 IL_008d: callvirt instance void [mscorlib]System.Threading.Thread::Start() 60 IL_0092: ldloca.s CS$5$0000 61 IL_0094: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::MoveNext() 62 IL_0099: brtrue IL_0011 63 IL_009e: leave.s IL_00ae 64 } // end .try 65 finally 66 { 67 IL_00a0: ldloca.s CS$5$0000 68 IL_00a2: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne> 69 IL_00a8: callvirt instance void [mscorlib]System.IDisposable::Dispose() 70 IL_00ad: endfinally 71 } // end handler 72 IL_00ae: ret 73 } // end of method IL::Test
问题主要出现在foreach语句中,因此我们对反编译语言只关心foreach的部分,但是为了表达完整性,其他部分也保留。经过分析,最后发现VS2010中在循环体开始时的指令是这么写的:
IL_000c: ldnull IL_000d: stloc.1 IL_000e: newobj instance void ILResearch.IL/'<>c__DisplayClass3'::.ctor() IL_0013: stloc.2 IL_0014: br.s IL_008f IL_0016: ldloc.2 IL_0017: ldloca.s CS$5$0000 IL_0019: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::get_Current() IL_001e: stfld class ILResearch.InstanceOne ILResearch.IL/'<>c__DisplayClass3'::item IL_0023: ldloc.2 //中间省略 IL_008f: ldloca.s CS$5$0000 IL_0091: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::MoveNext() IL_0096: brtrue IL_0016 IL_009b: leave.s IL_00ab
而VS2012是的指令是这样:
IL_000c: br IL_0092
IL_0011: ldnull
IL_0012: stloc.1
IL_0013: newobj instance void ILResearch.IL/'<>c__DisplayClass3'::.ctor()
IL_0018: stloc.2
IL_0019: ldloc.2
IL_001a: ldloca.s CS$5$0000
//中间省略。。。
IL_0092: ldloca.s CS$5$0000
IL_0094: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ILResearch.InstanceOne>::MoveNext()
IL_0099: brtrue IL_0011
IL_009e: leave.s IL_00ae
下面我们分析一下,相信很多童鞋们马上就明白了,VS2010在循环体开始时,首先创建临时对象c__DisplayClass3(指令 IL_0013),然后再指令IL_0016和IL_008f 之间做循环遍历list。VS2010只创建过一次c__DisplayClass3对象,每次循环时IL_001e修改c__DisplayClass3的属性Item,所以在因此只存在一个c__DisplayClass3对象,所以callback中拿到的对象属性Item已经被修改了。
但是vs2012完全不同,他做循环是在指令IL_0011 和 IL_0092中间,每次创建一个c__DisplayClass3对象,每个对象都有自己的属性Item,并且callback回调事件也是c__DisplayClass3的一个属性,因此callback拿到的item是自己独有的,没有被修改。
但是对于这么样的IL语言对于我们地球人看起来很费劲,至于我是怎么看出这个问题原因的,是从博客园老赵的博客中有一篇用Reflector查看不使用Optimize选项查看代码的文章中得到启发,用Reflector查看了反编译代码:
这个是Reflector反编译的VS2010的代码(Test方法):
1 public unsafe void Test() 2 { 3 Thread thread; 4 Action CS$<>9__CachedAnonymousMethodDelegate2; 5 <>c__DisplayClass3 CS$<>8__locals4; 6 List<InstanceOne>.Enumerator CS$5$0000; 7 CS$5$0000 = this._instances.GetEnumerator(); 8 Label_000C: 9 try 10 { 11 CS$<>9__CachedAnonymousMethodDelegate2 = null; 12 CS$<>8__locals4 = new <>c__DisplayClass3(); 13 goto Label_008F; 14 Label_0016: 15 CS$<>8__locals4.item = &CS$5$0000.Current; 16 if (CS$<>8__locals4.item.Index != 1) 17 { 18 goto Label_008F; 19 } 20 Console.WriteLine("Before callback Item index is " + ((int) CS$<>8__locals4.item.Index)); 21 thread = new Thread(new ThreadStart(this.CallBackInvoke)); 22 if (CS$<>9__CachedAnonymousMethodDelegate2 != null) 23 { 24 goto Label_0079; 25 } 26 CS$<>9__CachedAnonymousMethodDelegate2 = new Action(CS$<>8__locals4.<Test>b__1); 27 Label_0079: 28 this.CallBack = (Action) Delegate.Combine(this.CallBack, CS$<>9__CachedAnonymousMethodDelegate2); 29 thread.Start(); 30 Label_008F: 31 if (&CS$5$0000.MoveNext() != null) 32 { 33 goto Label_0016; 34 } 35 goto Label_00AB; 36 } 37 finally 38 { 39 Label_009D: 40 &CS$5$0000.Dispose(); 41 } 42 Label_00AB: 43 return; 44 }
这个是reflector反编译的VS2012的代码:
1 public unsafe void Test() 2 { 3 Thread thread; 4 Action CS$<>9__CachedAnonymousMethodDelegate2; 5 <>c__DisplayClass3 CS$<>8__locals4; 6 List<InstanceOne>.Enumerator CS$5$0000; 7 CS$5$0000 = this._instances.GetEnumerator(); 8 Label_000C: 9 try 10 { 11 goto Label_0092; 12 Label_0011: 13 CS$<>9__CachedAnonymousMethodDelegate2 = null; 14 CS$<>8__locals4 = new <>c__DisplayClass3(); 15 CS$<>8__locals4.item = &CS$5$0000.Current; 16 if (CS$<>8__locals4.item.Index != 1) 17 { 18 goto Label_0092; 19 } 20 Console.WriteLine("Before callback Item index is " + ((int) CS$<>8__locals4.item.Index)); 21 thread = new Thread(new ThreadStart(this.CallBackInvoke)); 22 if (CS$<>9__CachedAnonymousMethodDelegate2 != null) 23 { 24 goto Label_007C; 25 } 26 CS$<>9__CachedAnonymousMethodDelegate2 = new Action(CS$<>8__locals4.<Test>b__1); 27 Label_007C: 28 this.CallBack = (Action) Delegate.Combine(this.CallBack, CS$<>9__CachedAnonymousMethodDelegate2); 29 thread.Start(); 30 Label_0092: 31 if (&CS$5$0000.MoveNext() != null) 32 { 33 goto Label_0011; 34 } 35 goto Label_00AE; 36 } 37 finally 38 { 39 Label_00A0: 40 &CS$5$0000.Dispose(); 41 } 42 Label_00AE: 43 return; 44 }
从这个代码中和容易看出上面的问题,如果reflector optimize选项不选择为None是不能看出这个区别的,只能看到原代码。
3. 总结
通过上面的问题学会了很多方法,但是在平时学代码是还是需要写上break. 做一个有追求的程序员。