钻牛角尖之try return finally

 

       try catch finally是我们最常用的异常处理的流程,我们都知道执行try块代码,如果有异常发生就会被相应catch捕获到执行catch块代码,无论如何finally块的代码都会被执行。但是如果我们在try块中加入return语句,return和finally的执行顺序呢?

 

finally在return之前??

对此做过试验或者从finally总会被执行的作用来说,都会认为finally在return前执行。不过,看下面的例子。

js代码:

1
2
3
4
5
6
7
8
9
10
11
12
function testtry() {
        var i = 0;
        try {
            i = 1;
            return i;
        } catch (e) {
            i = 2;
            return i;
        } finally {
            i = 3;
        }
    }

 

.net代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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执行了吗

看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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代码的编译代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    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

1
2
3
4
5
6
7
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值不能影响到返回值;而对于引用类型,地址里保存的是指针,是不是应该是另一番光景呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

 

钻完收工。

posted @   for certain  阅读(3297)  评论(8编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示