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.
鑽完收工。