作者:Truly 来源:博客园   酷勤网收集 2007-08-25

摘要
  因为很多人使用.Net多年之后还是对异常处理一知半解的,有很多误解,本文将讲解三个常见误解,一个是catch的使用方法是否正确,另外两个是try/catch的性能损失问题。

作者:Truly
日期:2007.8.5

很久前就想写这么一篇文章,因为很多人使用.Net多年之后还是对异常处理一知半解的,有很多误解,本文将讲解三个常见误解,一个是catch的使用方法是否正确,另外两个是try/catch的性能损失问题。

有些人认为下面代码就是一个catch的错误用法:

catch(Exception e)
{
    
throw e;
}

首先说明,这不是一个错误用法,但是通常来讲,我们应该避免这种代码。然后要说明的是,这段代码有一个比较典型的作用就是改变异常出现的位置,也就是可以对某类异常统一在一个位置处理。先看下面代码:

    public int GetAllCount2()
    {
        
try
        {
            openDB();
            
int i = 1;
            
return i;
        }
        
catch (SqlException sex)
        {
            
throw sex;
        }
        
catch (Exception ex)
        {
            
throw ex;
        }
    }
    
public int GetAllCount()
    {
        openDB(); 
// 这里也可能是微软企业类库等
        int i = 1;
        
return i;
    }

    
private void openDB()
    {
        conn.Open();
    }

假设我们有一个公用方法叫openDB(),而很多方法中调用它,当数据库打开失败的时候,对于调用GetAllCount方法,异常将定位于conn.Open而如果调用GetAllCount2,那么异常定位于throw sex的位置,同时堆栈信息也有所不同,可以更快捷的找到调用方法的位置,也可在此位置进行一些错误恢复处理。尤其是我们编写一些底层类库的时候,比如Framework类库从不会把异常代码定位到Framework类库内部的某个方法上面。但是需要注意的是我们尽量避免捕获异常而不返回,例如
catch(){}

这样的使用就是典型的错误使用了,因为对于Framework来讲,任何时候系统都可能抛出一个StackOverflowException或者OutOfMemoryExcetpion而上面这段代码则隐藏了这些异常,有时候则导致一些严重的问题。


对于异常处理,在性能上有2点注意

第一点
,在使用try/catch时,如果不发生异常,那么几乎可以忽略性能的损失。

关于这一点,这里我们进行一些深入分析,对此比较了解的可以跳过本节。首先,让我们先看一下try/catch的IL表现。我们有2个方法,一个使用try/catch,而另一个未做任何处理:

static int Test1(int a, int b)
{
    
try
    {
        
if (a > b)
            
return a;
        
return b;
    }
    
catch
    {
        
return -1;
    }
}

static int Test2(int a, int b)
{
    
if (a > b)
        
return a;
    
return b;
}

使用ILDasm工具查看,IL代码分别如下:(这里之所以引入IL,是因为IL是比较接近机器汇编,所以在IL中我们可以更清楚的了解代码的执行情况,对IL没有兴趣的可以跳过此节)

.method private hidebysig static int32  Test1(int32 a,
                                              int32 b) cil managed
{
  // 代码大小       30 (0x1e)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000,
           [1] bool CS$4$0001)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldarg.0
    IL_0003:  ldarg.1
    IL_0004:  cgt
    IL_0006:  ldc.i4.0
    IL_0007:  ceq
    IL_0009:  stloc.1
    IL_000a:  ldloc.1
    IL_000b:  brtrue.s   IL_0011
    IL_000d:  ldarg.0
    IL_000e:  stloc.0
    IL_000f:  leave.s    IL_001b
    IL_0011:  ldarg.1
    IL_0012:  stloc.0
    IL_0013:  leave.s    IL_001b
  }  // end .try
  catch [mscorlib]System.Object 
  {
    IL_0015:  pop
    IL_0016:  nop
    IL_0017:  ldc.i4.m1
    IL_0018:  stloc.0
    IL_0019:  leave.s    IL_001b
  }  // end handler
  IL_001b:  nop
  IL_001c:  ldloc.0
  IL_001d:  ret
} // end of method Program::Test1


Test2

.method private hidebysig static int32  Test2(int32 a,
                                              int32 b) cil managed
{
  // 代码大小       22 (0x16)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000,
           [1] bool CS$4$0001)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldarg.1
  IL_0003:  cgt
  IL_0005:  ldc.i4.0
  IL_0006:  ceq
  IL_0008:  stloc.1
  IL_0009:  ldloc.1
  IL_000a:  brtrue.s   IL_0010
  IL_000c:  ldarg.0
  IL_000d:  stloc.0
  IL_000e:  br.s       IL_0014
  IL_0010:  ldarg.1
  IL_0011:  stloc.0
  IL_0012:  br.s       IL_0014
  IL_0014:  ldloc.0
  IL_0015:  ret
} // end of method Program::Test2

这里我们只需关注红字高亮的几行即可。此处我们只关心try区块,即未发生异常的时候,对于Test1来讲,IL代码多出了8个字节来保存catch的处理代码,这一点对性能和资源几乎是微不足道的。
我们看到当Test1执行到IL_000f或者IL_0013的时候,将数据出栈并使用leave.s退出try区块转向IL_001b地址,然后将数据入栈并返回。

对于Test2来讲,执行到IL_000e或者IL_0012的时候, 直接退出,并将数据入栈然后返回。

这里对几个关键指令简单介绍一下

nop      do noting
stloc.0  Pop value from stack into local variable 0.
ldloc.0  Load local variable 0 onto stack.
br.s target branch to target, short form
leave.s target Exit a protected region of code, short form

下面我们看代码的实际运行情况,新建一个控制台Console程序,加入下面代码:

   点击左边图标展开代码

运行后可以看到代码的差异,通常在0.0001%的差别以内。

第二点,如果发生异常,那么引发或处理异常时,将使用大量的系统资源和执行时间。引发异常只是为了处理确实异常的情况,而不是为了处理可预知的事件或流控制。例如,如果方法参数无效,而应用程序需要使用有效的参数调用方法,则可以引发异常。无效的方法参数意味着出现了异常情况。相反,用户偶尔会输入无效数据,这是可以预见的,因此如果用户输入无效,则不要引发异常。在这种情况下,请提供重试机制以便用户输入有效输入。

我们经常需要将一个字符串转换为int,比如将Request.QueryString["id"]这样的字符串转换为int,在asp.net 1.x时代,我们常使用下列方式

try
{
    
int id = Int32.Parse("123");
}
catch(){}

这样的后果是如果出现转换异常,你将不得不牺牲大量的系统资源来处理异常,即使你没有编写任何异常处理代码。

当然你也可以编写大量的代码来检测和转换字符串来替代try/catch方式,而从asp.net 2.0以后,框架将这个检测转换过程封装到Int32.TryParse方法中,再也不用蹩脚的try/catch来处理了。

还要补充一点,就是finally中的代码是始终保证运行的,所以留给大家一个问题,下面代码执行后a的值是多少:

int = 2;
try
{
    int i = Int32.Parse("s")
;
}
catch
{
    a 
= 1;
    return;
}
finally
{
    a 
= 3;
}



小节:本文主要对异常处理的3个常见误解进行了纠正。撰稿仓促,如有疏漏,烦请指出。

参考文献

http://msdn2.microsoft.com/zh-cn/library/system.exception(VS.80).aspx
http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/cpguide/html/cpconexceptionsoverview.asp
《CIL Instruction Set Specification》
《Applied Microsoft.NET Framework Programming》

 评论:

# re: 关于.NET的异常处理的几个误区[未登录]
2007-08-05 20:15 | Lucifer
这个在Jeffery Richter的CLR via C#中讲解的足够清楚,大家可以去阅读一下!
很遗憾的是楼主的
catch(Exception e)
{
throw e;
}
在.NET 1.x版本中不能捕捉不符合CLS的异常。而在.NET 2.0中虽然能够捕捉所有异常,却会改变异常的起始点。FxCop会报告这是个错误。

如果你要重新抛出异常,建议采用
catch(Exception e)
{
throw;
}
上面的这段代码虽然也会修改异常的起始点,但是CLR却知道原始异常被抛出时的堆栈位置。

此外,异常带来的好处远远超过它会带来的性能损失。  
  
# re: 关于.NET的异常处理的几个误区
2007-08-05 20:32 | 寸芒
楼上的,我有异议,虽然throw e 是抛出新的异常,但是这个新的异常对象还是原来的异常。就算你在这个子catch里改变了这个异常!  
  
# re: 关于.NET的异常处理的几个误区[未登录]
2007-08-05 20:52 | Lucifer
@寸芒
我同意你的观点。
实际上,这两段代码的区别就在于CLR如何确定异常抛出的起始点。
但是,如果你要用throw e。FxCop就会报错。

所以,还是推荐
catch(Exception e)
{
throw;
}
而这种捕获所有异常再次重新抛出的行为是非常罕见的。  
  
# re: 关于.NET的异常处理的几个误区
2007-08-06 08:35 | Anders Cui
建议:
尽量不要catch像Exception这样的通用异常类;
使用throw来维护异常堆栈;
如果要抛出新的异常,可以将原来的异常定为内部异常;
使用try-parse模式时注意,如果因为try操作之外的原因导致操作失败,仍应抛出异常;  
  
# re: 关于.NET的异常处理的几个误区
2007-08-06 09:35 | Bruce Lee
异常处理一直用不好,具体是什么,希望总结下。  
  
# re: 关于.NET的异常处理的几个误区
2007-08-06 10:08 | Truly
呵呵,首先看到大家踊跃发言,甚感欣慰,有些人也做了深入思考,这都很好,起到了抛砖引玉的效果。

顺便说一下,上面2位的意见基本上跟我的文中所述并没有什么出入,避免使用,改变异常位置,避免捕获所有异常等等这都是大家认可的观点。

另外,CLR via C#事实上就是我文中提到的参考文献中的《Applied Microsoft.NET Framework Programming》,在撰写此文的时候,我又再次审读了异常处理一章,不过还是谢谢你的留言。

同时,我也注意到Asp.NET 1.x到Asp.NET 2.0以后,异常有所不同,这一点我在测试那段代码的时候也注意到了,但是具体细节我没有去深究。  
# re: 关于.NET的异常处理的几个误区
2007-08-06 10:35 | Truly
对于一篇技术文章而言,能够留给读者一些思考空间是非常好的,显然本文达到了这一目的。对于技术,我也力图避免诱导、误导读者,多数情况我们分析原理,研究框架。这里分析出了try/catch时的情况,至于孰优孰劣,或者是优点多还是缺点多,这就交给读者见仁见智,我不喜欢教条式的论道  

分类: .NET技术 Windows技术



关于酷勤 | 联系方式 | 免责声明 | 友情链接