让我也来谈谈这个令人纠结的问题吧,说说我的见解,希望君能同意。
在MSDN,标准的有关 finally的描述是:*finally 块用于清除 try 块中分配的任何资源,以及运行任何即使在发生异常时也必须执行的代码。控制总是传递给 finally 块,与 try 块的退出方式无关。
*这句话底下还有一个备注:*catch 用于处理语句块中出现的异常,而 finally 用于保证代码语句块的执行,与前面的 try 块的退出方式无关。
*这意思也很好理解了,catch用于处理异常,而finally用于运行必须运行的代码,比如回收资源等。
那有人就要问了:finally 里的语句一定会被执行吗?我难道不可以使用何种语句让它不会执行吗?确实会在异常、崩溃时也能执行吗?
我一直这样回答提问者的问题:一定会,无论使用何种语句试图跳出,它都会被正常执行!
且不要急,下面,我将简单地论证为什么是这样的,以及什么时候 finally 里的语句不会被执行呢?
等等,我刚才不是说它一定会执行吗?那为什么现在又说它有可能不会被执行呢?
嗯,是这样的!我不是在唬人,确实有时候 finally 的语句不会被执行:就像那个很典型的问题“1加1什么时候不等于2”的答案一样:在没有执行语句的时候,或者CLR遇到不可抗力而无法正常运行程序的时候 finally 语句快就不会被运行。
而事实上呢?存在这样的时候吗?
当然存在!
这样吧,别人的话和代码如果不足以相信的话,那就回头再去看看MSDN吧,就连MSDN那个示例本身也存在很大问题!将它放到控制台程序中运行一下试试?发生了什么?!在开发时,VS会告诉你已经引发了异常;如果在运行时呢?好将它编译成程序,并手动运行看看,发生了什么?
如果你正好是 Vista 及以上系统(Windows Vista、Windows 7,应该还有 Windows Server 2008,未测试),你会发现控制台根本没有输出。
如果你是在 Vista 以下的系统,你会发现,确实是有输出的,不过控制台中可能被加入了一些关于异常信息的输出。
(注意,Mono的实例情况没有测试,但并不影响本文的结论;看本文中提到的 Windows 本身不也有不一致的情况吗?)
下面是上面MSDN示例的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void Main() { int i = 123; string s = "Some string"; object o = s; try { // Invalid conversion; o contains a string not an int i = (int)o; } finally { Console.Write("i = {0}", i); } } |
下面是截图:
这个不一样的表现如何解释呢?
而且,如果你此前还不知道,我现在还可以告诉亲爱的读者你:使用 .Net 中的语句 Application.Exit 、 Environment.Exit 等语句同样能阻止 finally 块的运行。
那我是在否定自己的结论吗?我刚才的命题还成立吗?一个由最简单的 finally 块都没有被正常运行,这如何为我的主张提供佐证?
好,在这里,我再一次声明:finally 块一定会被运行,如果你的程序真的在运行的话!
那就让我来为您解开这一层层迷惑吧:
首先,让我看看.Net程序执行的基本机制:
.Net 程序是在 CLR 或 Mono 等运行时(.Net Runtime Platform,.Net 运行平台,下同)中托管运行的,.Net 平台提供的异常处理机制是基于运行时的
CLR 等运行时是运行于操作系统基础上的,CLR等运行时必须遵守操作系统的规则和安排
不要觉得这些你似乎还看不太懂,请继续往下看。
这是个很简单的模型:就像人生活在一个社会中,人做什么事件都需要这个社会,然后人需要遵守社会的规则一样的。人就是程序,社会就是CLR等运行时。
理解了这些,那这个问题就变得简单了:
如果没有人,我们自然无法期待他还能开展社会活动;
如果.Net 程序没有在运行,那 finally 自然不会执行了。
很显然,当 .Net 程序被操作系统从CLR中终止之后,.Net 的任何代码都不会运行,而无论这语句是 finally 还是其他任何再神奇的语句。
好了,我们明白了,当 .net 程序的运行被终止时,代码不会执行。——读到这里,你会发现这个问题太他妈白痴了:程序终止了,代码还会运行吗?当然不能了!
但就是这样一个问题,那么多人却总是不能明白。
再让我们把目光焦点移回问题本身:为什么就连MSDN的示例代码提供的 finally 块都不会被正确地执行呢?(至少在有些操作系统上是这样,如 Vista 以上版本的 Windows)
就让我用上面的白痴话给你作答吧:因为当 try 中引发了异常、又没有使用 catch 处理时,程序会在运行时被操作系统挂起,然后询问用户是否调试(如果可能),直至终止。也就是说,很不幸,在这个 try 中,程序挂掉了!这样,你还在期待的 finally 块永远不会运行了……
那使用 Appllication.Exit 等语句的退出呢?finally 会被运行吗?我想这个问题,已经不需要问了。
总结一下:finally 块的代码一定会被执行:但有一个前提是程序继续在运行时托管环境运行着。
继续这个纠结的话题。
回到上面遇到的一点小状况:
1 为什么程序在在有些操作系统上 try..finally 中的 finally 被运行了,而当程序在另外一些操作系统中,finally 却没有被正确运行呢?
2 为了保障一些稀缺资源(如线程、IO操作、数据连接等)的有效回收,如何为程序提供真正有用、100%情况下都能运行的 finally 块呢?
第一个现象出现的原因是一些操作系统允许 try..finally 的做法,而新的操作系统内核中为了保障操作系统本身的稳定性,特意添加了一些新的特性,这些新的特性在程序发生异常时自动将它们终止(在终止前会询问用户是否调试,如果可能)。关于操作系统异常处理机制的更多资料,可以试图搜索 windows SEH 相关的知识。
第二个话题,我想说当你非得使用 Applicatiion.Exit 等语句退出时,事实胜于我说的任何语言,finally 语句是绝对不会执行的,因为程序已经终止。我想这样来询问这个问题的话,是一种不合理的挑战,有点砸馆的意味。
那真的就没有办法让 finally 在这种能预料的异常时运行吗?如果你是这样合理的询问,那就当然有!要不然我写这么多废话不是白写了吗?
一个很简单的方法就是在 finally 之前添加正确的 catch 块——这时,.Net 就认为你已经合理地处理了异常,异常情况也就不会报告给操作系统了,程序自然会继续运行下去。
是这样的,你会发现,现在已经出现了一个全新的命题:
在 Vista 以上的操作系统中,finally 中的语句一定不会被执行,当 try 语句中确实引发了异常,且程序中没有提供正确的 catch 块时。
这是一个十分严重的问题,请您对此倾注以十二分的注意!因为这意味着一些程序需要重新测试和编写。
好了,鉴于一些人在网上发了一些贴子提到或者讨论这个问题,我列举一些异常处理中应该注意的地方,同时给出几条建议:
1 在能预料到异常时,不要使用 try 的任何一种组合,因为这是低效的;
2 给 try 语句块添加 catch 语句块用于处理异常,即使用 try..catch..finally 而不是仅仅是 try..finally;
3 在可能引发多种异常的位置从最具体到最不具体的顺序排序所有 catch,并做不同的处理,而不仅仅是 catch(Exception e);
4 只捕获并处理能够处理的异常,而重新引发不应该处理的异常。有些异常是不应该被处理的,因为处理这些异常可能导致程序运行不可预料。这些异常包括 OutofMemoryException 和 StatckOverflowException;
5 不应该在 catch 中再使用可能引发异常的代码,除非是引发更明确、更合理或使用 throw; 引发相同的异常:catch 块是用来处理异常的,而不是引发异常的位置。
___________________________
参考资料:
1 有关此话题的一个小实验:http://www.cnblogs.com/opencoder/archive/2011/01/07/1929585.html
2 有关异常处理模型的一篇MSDN文章(英文):http://blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx
3 著作:《C# 捷径教程》
4 著作:《C#本质论》第三版
5 著作:《CLR via C#》第二版