为什么使用异常(Exceptions) 从错误代码转换到异常处理会对你的代码风格产生很大影响。要用一种不同的方法编写程序,需要你培养一套新的编程习惯,在你开始努力这么做之前,这篇文章会让你知道你的努力将非常有意义。
错误代码的使用已经有相当长的一段时间了。如果你在C++ 中编写代码,通常是下面的形式:
HRESULT retval = query.FetchRow(); if (FAILED(retval)) { // error handling here } |
使用错误代码的真正问题是返回的代码不是很好。尽管从理论上讲,错误代码可以正确处理程序中所有的错误,但总有一些问题是你无法预先知道的。一些传统编程模型的脆弱性就是源于不正确的错误处理;编写正确的代码并不容易.
问题的症结在于错误代码这种方法要求程序员去完成人类不擅长的工作--任何时候的一致性和完整性,并且系统环境没有给你任何帮助。异常(Exception)将使这一切变得容易很多。
是Opt Out而不是Opt In 错误代码和异常最重要的不同在于对"做正确的事情"的要求不一样。错误代码使用的是Opt in,如果代码中对错误没有做明确的处理,那么这个错误就会被忽略。
而异常(Exceptions)使用的是Opt Out模型。在默认情况下,运行时(runtime)会根据这些异常(exceptions)做正确的处理。
这个区别对编写的代码有着很重要的影响。因为运行时(Runtime)能对问题进行常规的处理,所以程序员就可以将精力放在那些需要特殊处理的问题。这可以使我们能用更少的精力却得到更为健壮的代码。光这一个好处就足以促成向异常(exception)的转变,更何况异常(exception)还有一个重要的优势。
更多的信息 在编写新代码时,写出的代码不能正常工作是很常见的。你可能会从某个函数那得到诸如"0x80001345"类似的返回值,接下来你就不得不想办法判断出其中的含义。
你首先要做的就是把这个返回值翻译成某种类型的符号代码。虽然可能会有更好的办法,但我通常是去查找 .h 文件中的常量。在找到之后,你需要打开MSDN来找出这代码的含义,最后才能决定如何修改程序。这还是在你幸运的情况下,有时候你更本就无法找到与错误码对应的符号代码。
有时候即使你找到了错误代码,也没有什么帮助。如果得到的错误码是ERROR_BAD_ARGUMENTS,我知道这是由给出的某个参数不正确引起的,但我不知道是哪一个。我调用的那个函数虽然知道,但它无法告诉我是哪一个出了问题。
有了异常机制,我除了能得到一个名字告诉我发生了哪一种错误,而且还能得到一个更具体的信息。例如,如果我传递了一个错误的参数,exception会告诉我哪一个参数错了。这节省了大量的时间。
更好的是,exception信息还能给出提示,教你如何去解决问题。今年早些时候,我试着将Beta1 的代码移植到Beta2中,当我运行程序的时候,程序抛出一个如下的异常(exception):
"Paint operation cannot be performed on this thread. Use Control.Invoke() instead" |
升级了的Beta2代码会检查不正确的用法,如果找到了,就抛出一个异常(exception)并确切地告诉程序员如何去解决这个问题。要修改这么一个错误真的是很简单。
异常(exception)处理和性能 当异常(exception)处理机制加入到C++时,它由于减慢了代码的执行速度而没得到好的声誉,使用了异常处理的代码可能会比不使用的慢一点。有一些不好的说法是可以理解的,尽管应该指出没有使用异常处理的代码会因为没有对众多错误进行检查而经常出问题。
随着时间的过去,C++中异常处理的开销减少了,但并没有减到零。为了恰当地实现异常处理,C++编译器必须在抛出异常(exception)的地方决定要创建哪些对象(这些对象使用完后需要释放),然后生成代码去检查异常(exception),并在异常检测完后正确地清理这些对象。这些代码会产生副作用,加大了代码优化的难度,所以还是会为此而损失性能。只要是使用C++,异常(exception)就会影响性能。
.NET世界使这一切对编译器编写者来说容易了很多。运行时(Runtime)对代码的运行了如指掌,.NET编译器不必再去检测异常的发生。更重要的是,对象能被垃圾收集器(garbage collector)所收集,所以不用再去分析对象的不同状态。当异常(exception)出现时,垃圾收集器会恰当地做好清理工作。这意味着在不出现异常(non-exceptional)的情况下不会有任何额外的开销。
使用异常处理(exception handling) 我想我已经让你深信异常处理(exception handling)是一个好的思想,现在该到了了解如何去写代码实现异常处理的时候了。
Throw 当一个异常情况出现时,它会被throw语句抛出。比如,如果一个函数需要一个非空字符串作为参数,它可能会含有下列代码:
public void Process(string location) { if (location == null) throw new ArgumentNullException("null value", "location"); } |
在这个例子中,用特定的信息和参数名创建了一个ArgumentNullException的新实例,并用throw语句将其抛出。
Try-Catch 编写错误处理最基本的结构就是try-catch。看下面的代码:
try { Process(currentLocation); Console.WriteLine("Done processing"); } catch (ArgumentNullException e) { // handle the exception here } |
在这例子中,如果try代码块(本例中就是Process()函数) 中的代码抛出一个ArgumentNullException异常,控制权就会马上转移到catch代码段,而不去执行Console.WriteLine()调用。
更多通用的Catching 在前面的例子中,catch语句捕获了一个ArgumentNullException,它和Process()抛出的异常相匹配。但是这并不是必需的。一个catch语句能捕获某指定的异常(exception)或任何从这个类继承的异常(exceptions)。例如:
try { Process(currentLocation); Console.WriteLine("Done processing"); } catch (ArgumentException e) { // handle the exception here } |
因为ArgumentNullException是