Net出现多年之后还是对异常处理一知半解的有很多误解本文将讲解三个常见误解一个是catch的使用方法是否正确另外两个是try/catch的性能损失问题
有些人认为下面代码就是一个catch的错误用法
catch(Exception e)
{
throw e;
}
首先说明这不是一个错误用法但是通常来讲我们应该避免这种代码然后要说明的是这段代码有一个比较典型的作用就是改变异常出现的位置也就是可以对某类异常统一在一个位置处理先看下面代码
public int GetAllCount()
{
try
{
openDB();
int i = ;
return i;
}
catch (SqlException sex)
{
throw sex;
}
catch (Exception ex)
{
throw ex;
}
}
public int GetAllCount()
{
openDB(); // 这里也可能是微软企业类库等
int i = ;
return i;
}
private void openDB()
{
connOpen();
}
假设我们有一个公用方法叫openDB()而很多方法中调用它当数据库打开失败的时候对于调用GetAllCount方法异常将定位于 connOpen而如果调用GetAllCount那么异常定位于throw sex的位置同时堆栈信息也有所不同可以更快捷的找到调用方法的位置也可在此位置进行一些错误恢复处理尤其是我们编写一些底层类库的时候比如 Framework类库从不会把异常代码定位到Framework类库内部的某个方法上面但是需要注意的是我们尽量避免捕获异常而不返回例如 catch(){}
这样的使用就是典型的错误使用了因为对于Framework来讲任何时候系统都可能抛出一个StackOverflowException或者OutOfMemoryExcetpion而上面这段代码则隐藏了这些异常有时候则导致一些严重的问题
对于异常处理在性能上有点注意
第一点在使用try/catch时如果不发生异常那么几乎可以忽略性能的损失
关于这一点这里我们进行一些深入分析对此比较了解的可以跳过本节首先让我们先看一下try/catch的IL表现我们有个方法一个使用try/catch而另一个未做任何处理
static int Test(int a int b)
{
try
{
if (a > b)
return a;
return b;
}
catch
{
return ;
}
}
static int Test(int a int b)
{
if (a > b)
return a;
return b;
}
使用ILDasm工具查看IL代码分别如下(这里之所以引入IL是因为IL是比较接近机器汇编所以在IL中我们可以更清楚的了解代码的执行情况对IL没有兴趣的可以跳过此节)
thod private hidebysig static int Test(int a
int b) cil managed
{
// 代码大小 (xe)
maxstack
locals init ([] int CS$$
[] bool CS$$)
IL_: nop
try
{
IL_: nop
IL_: ldarg
IL_: ldarg
IL_: cgt
IL_: ldci
IL_: ceq
IL_: stloc
IL_a: ldloc
IL_b: brtrues IL_
IL_d: ldarg
IL_e: stloc
IL_f: leaves IL_b
IL_: ldarg
IL_: stloc
IL_: leaves IL_b
} // end try
catch [mscorlib]SystemObject
{
IL_: pop
IL_: nop
IL_: ldcim
IL_: stloc
IL_: leaves IL_b
} // end handler
IL_b: nop
IL_c: ldloc
IL_d: ret
} // end of method Program::Test
Test
thod private hidebysig static int Test(int a
int b) cil managed
{
// 代码大小 (x)
maxstack
locals init ([] int CS$$
[] bool CS$$)
IL_: nop
IL_: ldarg
IL_: ldarg
IL_: cgt
IL_: ldci
IL_: ceq
IL_: stloc
IL_: ldloc
IL_a: brtrues IL_
IL_c: ldarg
IL_d: stloc
IL_e: brs IL_
IL_: ldarg
IL_: stloc
IL_: brs IL_
IL_: ldloc
IL_: ret
} // end of method Program::Test
这里我们只需关注红字高亮的几行即可此处我们只关心try区块即未发生异常的时候对于Test来讲IL代码多出了个字节来保存catch的处理代码这一点对性能和资源几乎是微不足道的
我们看到当Test执行到IL_f或者IL_的时候将数据出栈并使用leaves退出try区块转向IL_b地址然后将数据入栈并返回
对于Test来讲执行到IL_e或者IL_的时候 直接退出并将数据入栈然后返回
这里对几个关键指令简单介绍一下
nop do noting
stloc Pop value from stack into local variable
ldloc Load local variable onto stack
brs target branch to target short form
leaves target Exit a protected region of code short form
下面我们看代码的实际运行情况新建一个控制台Console程序加入下面代码
static void Main(string[] args)
{
int times = ; //我们将结果放大倍
long l lll s s;
ConsoleWriteLine(Press any key to continue);
ConsoleRead();
for (int j = ; j < ; j++)
{
l = DateTimeNowTicks;
for (int i = ; i < times; i++)
Test( );
l = DateTimeNowTicks;
s = l l;
ConsoleWriteLine(time spent: + s);
l = DateTimeNowTicks;
for (int i = ; i < times; i++)
Test( );
l = DateTimeNowTicks;
s = l l;
ConsoleWriteLine(time spent: + s);
ConsoleWriteLine(difference:+(ss)+rate:+(float)(ss)/s/times); }
}
static int Test(int a int b)
{
try
{
for (int i = ; i < ; i++) ; // 模拟长时操纵
if (a > b)
return a;
return b;
}
catch
{
return ;
}
}
static int Test(int a int b)
{
for (int i = ; i < ; i++) ; // 模拟长时操纵
if (a > b)
return a;
return b;
}
运行后可以看到代码的差异通常在%的差别以内
第二点如果发生异常那么引发或处理异常时将使用大量的系统资源和执行时间引发异常只是为了处理确实异常的情况而不是为了处理可预知的事件或流控制例如如果方法参数无效而应用程序需要使用有效的参数调用方法则可以引发异常无效的方法参数意味着出现了异常情况相反用户偶尔会输入无效数据这是可以预见的因此如果用户输入无效则不要引发异常在这种情况下请提供重试机制以便用户输入有效输入
我们经常需要将一个字符串转换为int比如将RequestQueryString[id]这样的字符串转换为int在 x时代我们常使用下列方式
try
{
int id = IntParse();
}
catch(){}
这样的后果是如果出现转换异常你将不得不牺牲大量的系统资源来处理异常即使你没有编写任何异常处理代码
当然你也可以编写大量的代码来检测和转换字符串来替代try/catch方式而从 以后框架将这个检测转换过程封装到IntTryParse方法中再也不用蹩脚的try/catch来处理了
还要补充一点就是finally中的代码是始终保证运行的所以留给大家一个问题下面代码执行后a的值是多少
int a = ;
try
{
int i = IntParse(s);
}
catch
{
a = ;
return;
}
finally
{
a = ;
}
小节本文主要对异常处理的个常见误解进行了纠正撰稿仓促如有疏漏烦请指出