C# Exception的用法和注意事项
正确使用异常处理可以让代码逻辑变得清晰,使程序的鲁棒性更好,并可以准确捕捉到一些细节的错误。那么问题来了,异常的使用场景是什么,实践中又有哪些常见问题,本文简单讨论一二。
何时用Exception?
《The Pragmatic Programmer》第24节探讨了这个问题:When to Use Exceptions? 在程序中常常要检查各种可能存在的错误,但检查可能存在于比较深层的嵌套或多层函数调用之中,于是代码会变得非常难看。问题主要存在于两个方面:
- 如何将底层调用的错误处理传回顶层?
- 是否要对下层调用的返回值进行检查并处理?
笔者在code review中见过不少蹩脚的Exception使用,根本原因在于没有理解上面两个问题。Exception使用不当不仅不能使代码变得简洁,反而会隐藏底层的问题,并且掩盖了主逻辑想要做什么。笔者将这种异常使用方法叫做“满篇的try-catch-check-null”(切克闹切克闹) :-),生怕你看懂代码真正想要做什么。
try
{
var book = GetBook(id);
if (book == null)
{
Logger.Error("Book is null");
}
...
}
catch (Exception ex)
{
Logger.Error(ex.Message);
throw;
}
这段“切克闹”其实有用的就一行代码var book = GetBook(id);
,但却写了十几行。更恶心之处在于catch
Exception之后又re-throw,只为输出一行错误日志。但这还算好的,至少catch的是GetBook()里的Exception。还见过更奇葩的用法,把try-catch当goto用:
try
{
if (condition)
{
throw new CustomException();
}
...
}
catch (Exception ex)
{
Logger.Error(ex.Message);
}
这样的用法自产自销除了增加开销,与goto有什么区别?把catch中的处理异常语句直接放在if字段中即可。
再引用《The Pragmatic Programmer》中的例子:
retcode = OK;
if (socket.read(name) != OK)
{
retcode = BAD_READ;
}
else
{
processName(name);
if (socket.read(address) != OK)
{
retcode = BAD_READ;
}
else
{
processAddress(address);
if (socket.read(telNo) != OK)
{
retcode = BAD_READ;
}
else
{
...
}
}
}
return retcode;
在主流程中判断了每个中间处理函数可能出现的错误,导致if嵌套很多,喧宾夺主,主逻辑不清晰。如果使用exception改进,代码清晰不少:
retcode = OK;
try
{
socket.read(name);
processName(name);
socket.read(address);
processAddress(address);
socket.read(telNo);
...
}
catch (IOException e)
{
retcode = BAD_READ;
Logger.Error("Error reading individual: " + e.Message);
}
return retcode;
}
并且这只是个非常简单的示例,实践中对于调用层数很深的代码,只在顶层捕捉Exception可以大大简化中间每层的检查逻辑,同时可以轻松地将底层的错误直接传递回顶层。
但Exception其实是一种类似goto的语句,如果不当使用,反而会降低程序的可维护性和效率。因此,作者给出建议:
Tips: Use Exceptions for Exceptional Problems
而对于什么是exceptional
,要根据情况而定。比如一个变量为null,那么抛异常好还是return
null好?判断标准是要看null的出现有多么频繁,以及它是否影响了主流程的继续。如果null在80%的时候都会出现,则它不是exceptional的,
而变成了normal。抛异常的目的是警示,这种情况下如果遇到null就抛异常,起不到警示的作用,反倒由于频繁处理异常导致性能降低。因此,此时使用return
null进行处理更为妥当。
用Exception时不要做什么
这里的建议就与C#语言相关了,本段摘自:Things to Avoid When Throwing Exceptions
The following list identifies practices to avoid when throwing exceptions: - Exceptions should not be used to change the flow of a program as part of ordinary execution. Exceptions should only be used to report and handle error conditions. - Exceptions should not be returned as a return value or parameter instead of being thrown. - Do not throw System.Exception, System.SystemException, System.NullReferenceException, or System.IndexOutOfRangeException intentionally from your own source code. - Do not create exceptions that can be thrown in debug mode but not release mode. To identify run-time errors during the development phase, use Debug Assert instead.
这里特别说下第三条:throw new Exception("xxx")。在上面Programming Guide上只说不要这么做,却没解释是为什么。原因在这个帖子中有讨论:# Why are we not to throw these exceptions?
总结起来,如果直接new Exception()的话,第一Exception太宽泛,相当于啥也没说,Exception的定义讲究尽量详细和具体,以便给调用者提供明确的错误信息。第二外面的try block中就必须catch (Exception),即catch所有的异常,这也是不好的做法。同理,MSDN上也不建议抛ApplicationException。其实与其这样告诉大家Guideline,还不如直接将Exception声明为abstract,这样就无法创建实例了,也就从根本上避免了这种做法。个人猜想可能因为兼容性的问题才没有改成这样。。
至于NullReferenceException和IndexOutOfRangeException,它们是由系统在运行时自动抛出的异常,属于“reserved exception”,手动抛这样的异常出去就很不合理。# Exception Handling (C# Programming Guide)