钻牛角尖之try return finally
try catch finally是我们最常用的异常处理的流程,我们都知道执行try块代码,如果有异常发生就会被相应catch捕获到执行catch块代码,无论如何finally块的代码都会被执行。但是如果我们在try块中加入return语句,return和finally的执行顺序呢?
finally在return之前??
对此做过试验或者从finally总会被执行的作用来说,都会认为finally在return前执行。不过,看下面的例子。
js代码:
function testtry() { var i = 0; try { i = 1; return i; } catch (e) { i = 2; return i; } finally { i = 3; } }
.net代码:
private Int32 TestTry() { Int32 i = 0; try { i = 1; return i; } catch { i = 2; return i; } finally { i = 3; } }
结果应该是1还是3呢?如果finally在return之前那应该是3啊,但是上面两段代码是执行是一个结果:1。
难道函数或方法遇到return直接返回,finally根本就没有执行??这不是和finally总会被执行的作用矛盾吗?
finally执行了吗
看这段代码:
function testtry() { var i = 0; try { i = 1; return i; } catch (e) { i = 2; return i; } finally { i = 3; return i; } }
因为.net不允许在finally中加return,因此没有了.net版本的这段代码。
这段js代码比之前的只是在return中多了一个return,结果应该是什么?1 or 3?
答案是3,这又能说明什么?它说明不管return和finally的执行顺序是怎样,finally肯定是被执行了。
那问题又来了,既然finally肯定被执行了,那我们的第一段代码结果就应该是3,而不应该是1啊?
原因揭秘
如何揭秘,我们就要借助第一段代码中.net代码的编译代码:
18: { 00000000 55 push ebp 00000001 8B EC mov ebp,esp 00000003 57 push edi 00000004 56 push esi 00000005 53 push ebx 00000006 83 EC 38 sub esp,38h 00000009 8B F1 mov esi,ecx 0000000b 8D 7D C8 lea edi,[ebp-38h] 0000000e B9 0B 00 00 00 mov ecx,0Bh 00000013 33 C0 xor eax,eax 00000015 F3 AB rep stos dword ptr es:[edi] 00000017 8B CE mov ecx,esi 00000019 33 C0 xor eax,eax 0000001b 89 45 E4 mov dword ptr [ebp-1Ch],eax 0000001e 89 4D C4 mov dword ptr [ebp-3Ch],ecx 00000021 83 3D 10 29 DD 03 00 cmp dword ptr ds:[03DD2910h],0 00000028 74 05 je 0000002F 0000002a E8 E8 52 DE 6A call 6ADE5317 0000002f 33 D2 xor edx,edx 00000031 89 55 C0 mov dword ptr [ebp-40h],edx 00000034 33 D2 xor edx,edx 00000036 89 55 BC mov dword ptr [ebp-44h],edx 00000039 90 nop 19: Int32 i = 0; 0000003a 33 D2 xor edx,edx 0000003c 89 55 C0 mov dword ptr [ebp-40h],edx 20: try 21: { 0000003f 90 nop 22: i = 1; 00000040 C7 45 C0 01 00 00 00 mov dword ptr [ebp-40h],1 23: return i; 00000047 8B 45 C0 mov eax,dword ptr [ebp-40h] 0000004a 89 45 BC mov dword ptr [ebp-44h],eax 0000004d 90 nop 0000004e C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0 00000055 C7 45 E4 FC 00 00 00 mov dword ptr [ebp-1Ch],0FCh 0000005c 68 8D 18 E5 03 push 3E5188Dh 00000061 EB 29 jmp 0000008C 24: } 25: catch 00000063 90 nop 26: { 00000064 90 nop 27: i = 2; 00000065 C7 45 C0 02 00 00 00 mov dword ptr [ebp-40h],2 28: return i; 0000006c 8B 45 C0 mov eax,dword ptr [ebp-40h] 0000006f 89 45 BC mov dword ptr [ebp-44h],eax 00000072 E8 61 0A B3 6A call 6AB30AD8 00000077 C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0 0000007e C7 45 E4 FC 00 00 00 mov dword ptr [ebp-1Ch],0FCh 00000085 68 84 18 E5 03 push 3E51884h 0000008a EB 00 jmp 0000008C 29: } 30: finally 31: { 0000008c 90 nop 32: i = 3; 0000008d C7 45 C0 03 00 00 00 mov dword ptr [ebp-40h],3 33: } 00000094 90 nop 00000095 58 pop eax 00000096 FF E0 jmp eax 00000098 90 nop 34: } 00000099 8B 45 BC mov eax,dword ptr [ebp-44h] 0000009c 8D 65 F4 lea esp,[ebp-0Ch] 0000009f 5B pop ebx 000000a0 5E pop esi 000000a1 5F pop edi 000000a2 5D pop ebp 000000a3 C3 ret 000000a4 C7 45 E4 00 00 00 00 mov dword ptr [ebp-1Ch],0 000000ab EB EB jmp 00000098 000000ad C7 45 E4 00 00 00 00 mov dword ptr [ebp-1Ch],0 000000b4 EB E2 jmp 00000098
这其实是.net执行的真实路径。
1,首先第一个我们可以看到的是 倒数第5行的ret指令,这个是返回指令,也就是说我们表面的return其实并不是真实的方法出口的位置。
2,看下return i;的IL
00000047 8B 45 C0 mov eax,dword ptr [ebp-40h] 0000004a 89 45 BC mov dword ptr [ebp-44h],eax 0000004d 90 nop 0000004e C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0 00000055 C7 45 E4 FC 00 00 00 mov dword ptr [ebp-1Ch],0FCh 0000005c 68 8D 18 E5 03 push 3E5188Dh 00000061 EB 29 jmp 0000008C
我们看到除了一些mov操作之外,并没有中止方法执行,最后一句是jmp跳转的指令,而这个跳转的地址正好是finally块的开始的地址,也就是这句执行之后去执行了finally。
3,在具体分析return i的IL之前,我们先看一下方法开始和结束时那两段没有特定对应的C#代码的IL,它们分别是“开场”prologue code:负责在方法开始之前对方法进行初始化,其中最重要是为方法的局部变量在线程堆栈上分配内存,并且为返回值分配内存,从代码中可以看到其中分配了和初始化了[ebp-40h] [ebp-44h] 这两块地址;“收场”代码epilogue code:方法完成清理并返回调用者。
4,接着上一块来说,我们可以看到每次操作i值时,都会有mov dword ptr [ebp-40h] 这样的操作,也就是说这块地址存储的是i的值。那么我们从return i;的IL代码可以看到它首先执行了两个操作:把[ebp-40h]的值给eax,然后又把eax值给了[ebp-44h],也就是其实返回值保存在了[ebp-44h]这个地址。
5,到最后,只是把[ebp-44h]这个地址的值放到数据寄存器,最后被调用者获取。
从此真相大白,也就是变量和返回值是分别保存在两个不同的地方,return i;时只用i值填充返回值的地址,finally时再次改变i的值,却不会影响返回值。至于js finally里能再次return i;也可能是再次修改了返回值那块地址所保存的值。
引用类型呢
对于值类型,分配的地址保存直接是值,再次修改i值不能影响到返回值;而对于引用类型,地址里保存的是指针,是不是应该是另一番光景呢?
function testtry2() { var o = {}; o.i = 0; try { o.i = 1; return o; } catch (e) { o.i = 2; return o; } finally { o.i = 3; } }
大胆猜一次返回的对象的i的值吧,对,就是3.
钻完收工。