.NET中异常处理非常好的实践
更多经验
问题在于过于普通的异常处理者。MSDN的文档中提及,Convert.ToInt32仅仅抛出ArgumentException,FormatException和OverflowException。所以,这些是唯一应该被处理的异常。
问题在于我们的配置没有包含第二个集合(GenericLibrary)。现在,当我们调用ConvertToInt时就会有一个FileNotFoundException的产生,同时代码假定它是由输入的值无效产生的。
下一次你书写“catch(Exception ex)”时,尽量描述清楚OutOfMemoryException异常被抛出时,你的代码该如何处理。
不要总是吞掉异常
你做的最糟糕的事情是在catch (Exception)后加了一个空的模块。永远不要这样做。
清理代码应该放在finally模块中
理论上,由于你并没有处理许多普通的异常,同时你拥有一个中央异常处理函数,你的代码应该有远比catch模块多的finally模块。不要把处理代码,如关闭流,恢复状态(就像鼠标指针)放在finally模块之外。要养成习惯。
人们经常忽略的一件事是try/finally 模块如何使你的代码变得更加可读与健壮。这是处理代码的巨大作用所在。
做为一个例子,假设你需要从一个文件中阅读一些临时信息,然后以字符串的形式返回它。不管发生什么,你都必须删除这一文件,因为它是临时的。这样的返回处理功能需要try/finally模块来完成。
让我们看没有使用try/finally模块的最简单的代码:
这段代码在抛出异常时同样遇到一个问题。比如,ReadToEnd函数:它在硬盘上留下临时文件。因此,我真实的看到有人想用如下代码来解决:string ReadTempFile(string FileName)
...{
string fileContents;
using (StreamReader sr = new StreamReader(FileName))
...{
fileContents = sr.ReadToEnd();
}
File.Delete(FileName);
return fileContents;
}
代码开始变的复杂的同时也开始复制代码string ReadTempFile(string FileName)
...{
try
...{
string fileContents;
using (StreamReader sr = new StreamReader(FileName))
...{
fileContents = sr.ReadToEnd();
}
File.Delete(FileName);
return fileContents;
}
catch (Exception)
...{
File.Delete(FileName);
throw;
}
}
现在,我们来看看使用try/finally的方法使代码变的多麽的整洁和健壮:
string ReadTempFile(string FileName)
...{
try
...{
using (StreamReader sr = new StreamReader(FileName))
...{
return sr.ReadToEnd();
}
}
finally
...{
File.Delete(FileName);
}
}
fileContents变量哪里去了?它不再需要,因为我们可以返回内容后使得处理代码执行。这是拥有可以在函数返回后执行的代码的优势之一:你可以清空可能在返回状态时依然需要的资源。
经常使用using
仅仅在一个对象上调用Dispose()函数是远远不够的。关键字using将会阻止资源泄漏即使在有异常出现的地方。
不要在错误条件下返回特殊值
特殊值存在很多问题:
•异常使得普通的事件更快,因为当你从函数返回特殊值时,每一个函数返回需要被检查,这个过程至少消耗一个进程寄存器或者更多,这些导致了代码的运行缓慢。
• 特殊值可以或者将被忽略。
• 特殊值不携带堆栈追踪,可以丰富错误细节
• 经常发生的情况是函数没有恰当的可以反映错误情况的值返回。为表示“被0除”这一错误,你该让如下函数返回什么值呢?
public int divide(int x, int y)
{
return x / y;
}
不要使用异常去暗示资源的丢失
微软建议在极端的普通情况下你应该使用返回特殊值。我知道我写的恰恰与之相反,我也不想这样,但是大多数API一致时生活会变得更加容易,所以我建议你谨慎的遵守这条法则。
我观察.net框架,注意到几乎使用这一风格的唯一的API是那些返回一定资源的API(如 Assembly.GetMnifestStream 方法)。所有的这些API在缺乏资源的情况下均返回空。
不要把异常处理方法作为从函数中返回信息的手段
这是一个极差的设计。不仅异常的处理缓慢(就像名字暗示的一样,他们意味着只被使用在异常情况),而且代码中许多的try/catch模块会导致代码很难维护。恰当的类设计可以提供普通的返回值。如果你确实在危机中想返回数据作为一个异常,那么你的方法可能做了太多的工作需要分解。
为那些不该被忽略的错误使用异常
我使用现实世界的例子来说明这个问题。在开发一个API以便人们可以访问Crivo(我的产品)的时候,你应该做的第一件事是调用Login函数。如果Login失败,或未被调用,其他的每个函数调用将会失败。我的选择是如果Login函数调用失败就从中抛出一个异常,而不是简单的返回错误,这样调用程序就不能忽略它。
当再次抛出异常时不要清空堆栈追踪
堆栈追踪是一个异常携带的最有用的信息之一。经常,我们需要在catch模块中,放入一些异常处理代码(如,回滚一个事务)然后再抛出异常。看它正确(错误)的处理方法:错误的处理方法:
try
{
// Some code that throws an exception
}
catch (Exception ex)
{
// some code that handles the exception
throw ex;
}
为什麽这个是错误的呢?因为,当你检查堆栈跟踪时,异常将会运行到“throw ex”这一行,隐藏了真实的出错位置。你可以试一下。
try
{
// Some code that throws an exception
}
catch (Exception ex)
{
// some code that handles the exception
throw;
}
观察以上代码什么改变了呢?取代了这个将会抛出新异常同时清空堆栈追踪的“throw ex;”语句,我们使用了简单的“throw;”语句。如果你没有指定这个异常,throw 声明将会仅仅再次抛出catch声明捕获的异常。这将会保证你的堆栈追踪完整无缺,但是依然允许你在catch模块中放入代码。
避免在没有增加语义值时就改变异常
只有在需要给它增加一些语义值时,你才可以改变一个异常。比如,你在做一个DBMS连接驱动驱动,以便用户可以不必担心特殊的socket错误而仅仅需要知道连接失败。
如果你总是需要这样做,那么,请在InnerException成员中保持最初的异常。不要忘记你的异常处理代码中也许同样有漏洞,这样如果你有InnerException,你就会很容易的找到它。
异常应该用[Serializable]标识
大量的情形需要异常是可序列化的。当从另一个异常类继承的时候,不要忘记增添这一属性。你将永远都不知道,你的函数什么时候将被远程组件或服务器调用。
有疑惑时,不要断言,抛出异常
不要忘记Debug.Assert已经从释放代码中移除。在检查和确认的时候,在代码中抛出异常要比加入声明好一些。
为单元测试,内部循环变量,为那些由运行条件(如果你考虑的话,是非常稀有的条件)决定的永远不该出错控制保存声明。
每一个异常类都应该至少拥有三个初始化构造函数
做到这点是很容易的(仅仅是从其他异常类拷贝和复制定义)然而没有能这样不会允许使用你类的用户遵循以下的几条原则。
我提到的是那些构造函数呢?是这一页上最后描述的三个构造函数。
使用AppDomain.UnhandledException事件时要小心
修订笔记:在我的博客中,Philip Haack指出了这一重要遗漏。其他错误的共同源头是Application.ThreadException事件。使用它们时有如下诸多告诫:
• 异常通知出现的太晚:当你收到通知时,你的应用程序已经不能对异常作出反应了。
• 异常如果发生在主线程(事实上,是任何由无管理代码启动的线程)中,应用程序将会结束。
• 很难编写可以不间断工作的普通代码。引用MSDN的一段话:“这个事件仅仅发生在应用程序启动时由系统创建的应用程序领域。如果应用程序创建额外的应用程序领域,在哪些应用程序领域中为这一事件指定代表也是没有作用的。”
• 当代码处理这些事件时,除了异常本身你没有权力使用任何有用信息。你不能关闭数据连接,回滚事务,或其他有用的事情。对初学者来说,使用全局变量的诱惑是巨大的。
确实,你不应该把你全部的异常处理策略放在这些事件的基础上。想象他们是“安全网“的同时为未来的测试记录异常。之后,确保更正那些没有正确处理异常的代码。
