为什么使用异常(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是从ArgumentException继续而来的,所以这个catch语句也能捕捉ArgumentNullException异常。另外,它还能捕捉其他继续而来的异常:ArgumentOutOfRangeException、InvalidEnumArgumentException和
DuplicateWaitObjectException。
既然所有的异常(exceptions)最终都是从Exception类衍生出来的,那么只要对Exception类进行捕捉,就能捕捉任何其他的异常(exception)。C++没有限制用户只能抛出Exception的导出类,C#提供了一种能捕捉所有异常的语法:
catch
{
// handle the exception here
}
尽管这个语法适用于所有的异常,但它没什么实际利用价值。大部分C++程序员会选择抛出从Exception继续而来的异常,即使他们不这么做,这个catch 语法也没法让你确定抛出的是什么异常。
Catch语句的排列顺序
我们可以在一个try语句中放置多个catch语句,每个catch语句捕捉一种不同类型的异常。在前面这个例子中,对ArgumentException做一个非凡的处理是挺有必要的,对其它的异常可以进行另外的处理。
这个例子如下:
try
{
Process(currentLocation);
Console.WriteLine("Done processing");
}
catch (ArgumentException e)
{
// handle the exception here
}
catch (Exception e)
{
// handle the more general exception here
}
当使用了多个catch语句时,导出类型必须列在它的任何基类之前。这有助于提高可读性。你可以更早地判定出运行时(Runtime)的行为。
Catch Operations
现在我们捕捉了一个异常(exception),希望用它做一些有用的事。我们要做的第一件事就是想把这个异常用一些额外的上下文信息包装起来。
我们用下面的方法来实现:
try
{
Process(currentLocation);
Console.WriteLine("Done processing");
}
catch (ArgumentException e)
{
throw new ArgumentException("Error while processing", e);
}
这里使用了ArgumentException的构造器,它接受了一个信息和一个异常。构造器把传递给它的这个异常用一个新的异常包装了起来并抛出。
这个过程给开发者们提供了一种极大的便利。将异常包装起来就得到了类似于堆栈跟踪(stack trace)的结果,而不仅仅是一些关于异常的单一信息:
System.Exception: Exception in Test1
---> System.Exception: Exception in Test2
---> System.DivideByZeroException: Attempted to divide by zero.
假如你在编译时使用了/debug开关,这样的输出将使大大方便调试。你还可以得到每一级的文件名和行号。
异常(exceptioin)的包装在为调试提供额外的信息方面很有用。另外一种很有帮助的场合就是在你需要根据一个异常采取相应的行动的时候。把输出写入一个文件的实现代码应该像下面这样:
try
{
FileStream f = new FileStream(filename, FileMode.Create);
StreamWriter s = new StreamWriter(f);
s.WriteLine("{0} {1}", "test", 55);
s.Close();
f.Close();
}
catch (IOException e)
{
Console.WriteLine("Error opening file {0}", filename);
Console.WriteLine(e);
}
假如这个文件不能被打开,系统就抛出一个异常,catch语句就被触发,产生一个错误,程序可继续执行下去。在大多数情况下,这是没问题的。 但是当出现下面这种情况时问题就来了:一个异常出现在这个文件刚被打开后,这样该文件将无法被关闭,这是有害的。
这里需要的是一种能保证即使在异常发生的情况下,文件仍能被关闭的方法。
QQread.com
推出各大专业服务器评测 Linux服务器的安全性能
SUN服务器
HP服务器
DELL服务器
IBM服务器
联想服务器
浪潮服务器
曙光服务器
同方服务器
华硕服务器
宝德服务器
Try-Finally
Finally 这个结构用来指定一段代码,即使在异常发生的情况下,这段代码也能够保证运行。通常,finally语句用来负责在异常发生后的清理工作。我们把前面的例子修改如下:
try
{
FileStream f = new FileStream(filename, FileMode.Create);
StreamWriter s = new StreamWriter(f);
s.WriteLine("{0} {1}", "test", 55);
s.Close();
f.Close();
}
catch (IOException e)
{
Console.WriteLine("Error opening file {0}", filename);
Console.WriteLine(e);
}
finally
{
if (f != null)
f.Close();
}
在这段修改过的代码中,即使发生了异常,finally语句也将被执行。
Using和Lock语句
前面我们看到的是一个非经常用的模式,C#提供了一种非凡的支持以促使程序员们写出正确的代码。Using语句可以被用来创建类似try-finally的代码:
using (FileStream f = new FileStream(filename, FileMode.Create))
{
StreamWriter s = new StreamWriter(f);
s.WriteLine("{0} {1}", "test", 55);
s.Close();
f.Close();
}
catch (IOException e)
{
Console.WriteLine("Error opening file {0}", filename);
Console.WriteLine(e);
}
这段代码等同于前面的例子。
Lock语句提供了一个类似的对System.Threading.Monitor类的包装,以保证即使在异常发生的情况下锁(locks)也能被释放。
QQread.com
推出各大专业服务器评测 Linux服务器的安全性能
SUN服务器
HP服务器
DELL服务器
IBM服务器
联想服务器
浪潮服务器
曙光服务器
同方服务器
华硕服务器
宝德服务器
设计方针
这个部分将讲述一些异常处理的设计方针。
1、尽量捕捉明确的异常
假如你的代码需要从一些异常中恢复过来,则要确信只捕捉那些异常。假如你捕捉更多的通用异常,这就与你错误地忽略这些异常没什么区别。
2、只忽略你有把握的异常
这是由前面那个方针推断而出的。当你忽略一个异常时,意味着你应该知道这个异常能引起的所有可能情况,并且你编写的恢复代码能够处理所有的这些情况。
3、假如合适的话,尽量使用lock或using语句
假如你能使用lock或using 语句,就尽量使用它们。它们使代码更具可读性,而且能使得你更少犯错误。
4、假如可能的话,尽量包装异常
假如你可以往一个异常中加入额外的信息的话,要尽一切办法做到。假如我要往另一个函数传递一个参数,那么加入这个参数的额外信息对我来说是很有帮助的。
try
{
o.Process(v1, v2, v3);
}
catch (ArgumentException e)
{
throw new ArgumentException("error updating contact information", e);
}
Catch语句将捕捉到的表达式(eXPression)用同样的类型包装起来并抛出。这给了用户两个层次的信息。
然而使用这种方法时要当心,在这个例子中,catch语句能够捕捉ArgumentException异常和任何从这个类继续而来的异常(比如ArgumentNullException)。但我的调用者从来不会关心这些区别,只是抛出一个ArgumentException异常。
这种方法的最坏情况就是将一个Exception这个异常包装并再次抛出。假如调用者需要对这个异常的真实类型进行解码,则必需用下面的代码:
try
{
o.BadFunction();
}
catch (Exception e)
{
if (e.Inner.GetType() == typeof(ArgumentException))
{
}
else
throw;
}
这段代码检查每个被包装了的异常,要么处理它要么再次抛出它。假如你的用户不得不编写类似的代码,那么你的代码将受到严重影响。