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)

推荐阅读

# Best practices for exceptions