建议61:避免在finally内撰写无效代码
在阐述建议之前,需要先提出一个问题:是否存在一种打破try-finally执行顺序的情况,答案是:不存在(除非应用程序本身因为某些很少出现的特殊情况在try块中退出)。应该始终认为finally内的代码会在方法return之前执行,哪怕return在try块中。
正是这点,可能会让你写出无效的代码,有时候,这样的无效代码会是一个隐藏很深的Bug。
看下面代码:
private static int TestIntReturnBelowFinally() { int i; try { i = 1; } finally { i = 2; Console.WriteLine("\t将int结果改为2,finally执行完毕"); } return i; }
返回值是2。
但是:
private static int TestIntReturnInTry() { int i; try { return i = 1; } finally { i = 2; Console.WriteLine("\t将int结果改为2,finally执行完毕"); } }
返回值是1。
再看下面代码:
static User TestUserReturnInTry() { User user = new User() { Name = "Mike", BirthDay = new DateTime(2010, 1, 1) }; try { return user; } finally { user.Name = "Rose"; user.BirthDay = new DateTime(2010, 2, 2); Console.WriteLine("\t将user.Name改为Rose"); } }
user类:
class User { public string Name { get; set; } public DateTime BirthDay { get; set; } }
TestUserReturnInTry方法返回的User中,Name的值已经改为Rose了。
现在来解释为什么上面3个函数会有3种结果。查看TestIntReturnBelowFinally的finally部分的IL代码:
finally { IL_0004: ldc.i4.2 IL_0005: stloc.0 IL_0006: ldstr bytearray (09 00 06 5C 69 00 6E 00 74 00 D3 7E 9C 67 39 65 // ...\i.n.t..~.g9e 3A 4E 32 00 0C FF 66 00 69 00 6E 00 61 00 6C 00 // :N2...f.i.n.a.l. 6C 00 79 00 67 62 4C 88 8C 5B D5 6B ) // l.y.gbL..[.k IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: endfinally } // end handler IL_0011: ldloc.0 IL_0012: ret}
“IL_0004: ldc.i4.2”首先将2压入栈顶
“IL_0005: stloc.0”将最顶层堆栈的值,也就是2赋值给本地变量,也就是 i (index 0)
“IL_0011: ldloc.0”将本地变量 i (index 0)的值再次压入栈
“IL_0012: ret”结束函数,同时把栈内的返回值压入调用者的栈中。就函数将2赋值给了返回值。
看方法TestIntReturnInTry()的Debug版本的IL代码:
.method private hidebysig static int32 TestIntReturnInTry() cil managed{ // 代码大小 27 (0x1b) .maxstack 2 .locals init ([0] int32 i, [1] int32 CS$1$0000) IL_0000: nop .try { IL_0001: nop IL_0002: ldc.i4.1 IL_0003: dup IL_0004: stloc.0 IL_0005: stloc.1 IL_0006: leave.s IL_0018 } // end .try finally { IL_0008: nop IL_0009: ldc.i4.2 IL_000a: stloc.0 IL_000b: ldstr bytearray (09 00 06 5C 69 00 6E 00 74 00 D3 7E 9C 67 39 65 // ...\i.n.t..~.g9e 3A 4E 32 00 0C FF 66 00 69 00 6E 00 61 00 6C 00 // :N2...f.i.n.a.l. 6C 00 79 00 67 62 4C 88 8C 5B D5 6B ) // l.y.gbL..[.k IL_0010: call void [mscorlib]System.Console::WriteLine(string) IL_0015: nop IL_0016: nop IL_0017: endfinally } // end handler IL_0018: nop IL_0019: ldloc.1 IL_001a: ret} // end of method Program::TestIntReturnInTry
TestIntReturnInTry在IL中创建了两个本地变量 i 和CS$1$0000 ,i 存储的是1,然后finally中 i 被赋值为2。调用者真正得到的是由IL创建的CS$1$0000所对应的值。用Reflector查看C#代码:
private static int TestIntReturnInTry(){ int i; int CS$1$0000; try { CS$1$0000 = i = 1; } finally { i = 2; Console.WriteLine("\t将int结果改为2,finally执行完毕"); } return CS$1$0000;}
实际上,finally中i=2没有任何意义,所以在本函数的release版本中,IL中找不到对应的代码:
.method private hidebysig static int32 TestIntReturnInTry() cil managed{ // 代码大小 17 (0x11) .maxstack 1 .locals init ([0] int32 CS$1$0000) .try { IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: leave.s IL_000f } // end .try finally { IL_0004: ldstr bytearray (09 00 06 5C 69 00 6E 00 74 00 D3 7E 9C 67 39 65 // ...\i.n.t..~.g9e 3A 4E 32 00 0C FF 66 00 69 00 6E 00 61 00 6C 00 // :N2...f.i.n.a.l. 6C 00 79 00 67 62 4C 88 8C 5B D5 6B ) // l.y.gbL..[.k IL_0009: call void [mscorlib]System.Console::WriteLine(string) IL_000e: endfinally } // end handler IL_000f: ldloc.0 IL_0010: ret} // end of method Program::TestIntReturnInTry
用Reflector查看release版本中C#代码:
private static int TestIntReturnInTry(){ int CS$1$0000; try { CS$1$0000 = 1; } finally { Console.WriteLine("\t将int结果改为2,finally执行完毕"); } return CS$1$0000;}
再解释第三个方法TestUserReturnInTry为什么返回的是“Rose”。Reflector查看release版本中C#代码:
private static User TestUserReturnInTry(){ User CS$1$0000; User <>g__initLocal0 = new User { Name = "Mike", BirthDay = new DateTime(0x7da, 1, 1) }; User user = <>g__initLocal0; try { CS$1$0000 = user; } finally { user.Name = "Rose"; user.BirthDay = new DateTime(0x7da, 2, 2); Console.WriteLine("\t将user.Name改为Rose"); } return CS$1$0000;}
User是引用类型, CS$1$0000 = user;说明CS$1$0000和user指向的是同一个对象,当在finally中 user.Name = "Rose"时CS$1$0000的Name也会变为“Rose”。所以返回的CS$1$0000的Name为“Rose”。
再举一个例子:
private static User TestUserReturnInTry2() { User user = new User() { Name = "Mike", BirthDay = new DateTime(2010, 1, 1) }; try { return user; } finally { user.Name = "Rose"; user.BirthDay = new DateTime(2010, 2, 2); user = null; Console.WriteLine("\t将user置为anull"); } }
返回的结果不是null,而一个Name=“Rose”,BirthDay = new DateTime(2010, 2, 2)的User对象。Reflector查看release版本中C#代码:
private static User TestUserReturnInTry2(){ User CS$1$0000; User <>g__initLocal1 = new User { Name = "Mike", BirthDay = new DateTime(0x7da, 1, 1) }; User user = <>g__initLocal1; try { CS$1$0000 = user; } finally { user.Name = "Rose"; user.BirthDay = new DateTime(0x7da, 2, 2); user = null; Console.WriteLine("\t将user置为anull"); } return CS$1$0000;}
CS$1$0000和user指向的是同一个对象,当在finally中 user=null 时,只是user指向为null了,CS$1$0000指向的对象并没有变。
转自:《编写高质量代码改善C#程序的157个建议》陆敏技