NET中异常处理最佳实践

简介

    “我的软件从来不出错”你能相信吗?我几乎听到你们全部尖叫说我是个说慌者。“从不出错的软件从某种程度上讲是不可能的!”

    和普通人的观念相反,创造可信赖的,健壮的软件并不是一件不可能的事情。请注意,我并没有提及意欲控制核电站的无漏洞软件。我提到的仅仅是可以在无人看管 的服务器或者客户端机器上运行的普通的商业软件,在长时间(几个星期或是几个月)可以无重大故障的工作。可预测的,我的意思是它拥有低出错率,你可以迅速 理解出错原因然后快速搞定它,同是,它从不因为外部错误而毁坏数据。换句话说,软件是稳定的。软件中有漏洞是可以原谅的,甚至是被期望的。不可原谅的是您 无法解决一个复发的漏洞,仅仅是因为您没用足够的信息。

    为了更好的理解我所说的,我看过不计其数的商业软件,DBMS是这样报告空间不足的错误的:“不能更新用户操作,请与系统管理员联系后在尝试。”

    虽然,用这则消息向一个商业用户报告一种未知的资源失败也许是恰当的,通常它应该是可以用来调试错误原因的全部调试信息。但是,如果没用东西被记入日志,了解当时的状况将会是一件非常费时的事情,通常,程序员会猜测许多可能的原因直到他们找到真正的错误。

    在这篇文章中注意到这些,我将会集中精力介绍如何去更好的利用.NET的异常机制:我不会去讨论怎样正确的报告错误信息,因为我认为这个问题应该属于UI(用户界面)领域,同时它十分依赖正在开发的接口和所要面对的听众;一面向青少年的博客文章编辑者应该用一种与直接面向编程人员的socket server完全不同的方式来报告出错信息。

做好最坏的打算

    几个基础设计概念将会使你的程序更加健壮,同时提高用户处理意外错误的经验。“提高用户处理意外错误的经验”是什么意思呢?它是用户不会被你提供的令人惊 异的对话框吓的发抖。它更多的是不要使用户误用数据,搞垮计算机,以确保计算机运转的更加安全。如果你的程序可以无损坏的处理空间不足错误,你将会增加用 户的经验。

及早检查

    强大的类型检查和确认是防止意外异常,确保文档记录及代码检查的有力工具。发现问题的杀伤力越早,这个问题就越容易解决。设法在数月后去了解 InvoiceItems 表中Customerid在Productid栏目中的作用几乎是不可能的,这不是开玩笑。如果你使用类而不是原始数据类型(比如 int ,string 等)来存储客户数据,编译器将不会给你处理上述事件的机会。

不要相信外部数据

    外部数据是不可相信的。不管这些数据是来自寄存器,数据库,硬盘,socket,你所书写的文件抑或是键盘,它们都必须被广泛的检查。所有的外部数据都应 该被检查,只有这样你才可以信任它。我经常发现信任配置文件的程序,原因是它们的编写者从没想过有人会编辑或误用它。

唯一可信赖的外部设施是:显示器,鼠标和键盘

当你需要外部数据时,你可能遇到以下的情形:

◆没有足够的安全权限

◆信息不在那儿

◆信息不完整

◆信息是完整的但是不可用的

    不管它是注册信息,文件,SOCKET,数据库,网站服务抑或是串行端口,所有的外部数据源迟早都会出错。为安全错误做好准备才能把损失降到最小。


书写同样可能出错

不可靠的数据源同样是不可靠的数据仓库。当你保存数据时,类似的情况可能发生:

◆没有足够的安全权限

◆设备不在那儿

◆设备没有足够的空间

◆设备由物理错误

    以下便是压缩程序开始时创建临时文件,压缩完后重命名它而不是取代原来文件的原因:如果硬盘(或压缩软件)因某种原因出错,你将会丢失你的原始数据。

代码的安全性

    我的一个好朋友经常说:“一个好的程序员从不在他的项目中引用不安全的代码”。我不认为这是一个好的程序员的全部,但是的确几乎是这样。下面,我编译了一些常见的“不安全代码”,这些代码在经过异常处理后可以引用到你的项目中。

 

 

 

不要抛出new Exception()

    不要抛出 new Exception()。Exception是一个非常大的类,如果没有side-effect,很难去捕获。引用你自己的异常类,但是使它继承自 AppliationException。通过这种方法,你可以设计一个专门的异常捕获程序去捕获框架抛出的异常,同时设计另一个异常捕获程序来处理自己 抛出的异常。

    修订记录:在以下的评论部分中,David Levitt写信告诉我说,尽管Microsoft公司在MSDN doc中依然鼓吹使用System.ApplicationException做为基础类,但这已经不是一个好的习惯,就像Brad Adams在他的博客中所说的那样。这个方法是尽可能创造浅且宽泛的异常类层次,就像你经常处理类层次结构那样。我不马上改变文章内容的原因是在此介绍之 前,我需要做更多的研究。做完这项研究后,我依然不能决定浅的类层次结构在异常处理中是否是个好办法,所以,在此处我给出了两种观点。但是,无论你做什 么,不要抛出new Exception(),不要在需要时继承你自己的异常类。

 

 

不要抛出new Exception()

    不要抛出 new Exception()。Exception是一个非常大的类,如果没有side-effect,很难去捕获。引用你自己的异常类,但是使它继承自 AppliationException。通过这种方法,你可以设计一个专门的异常捕获程序去捕获框架抛出的异常,同时设计另一个异常捕获程序来处理自己 抛出的异常。

    修订记录:在以下的评论部分中,David Levitt写信告诉我说,尽管Microsoft公司在MSDN doc中依然鼓吹使用System.ApplicationException做为基础类,但这已经不是一个好的习惯,就像Brad Adams在他的博客中所说的那样。这个方法是尽可能创造浅且宽泛的异常类层次,就像你经常处理类层次结构那样。我不马上改变文章内容的原因是在此介绍之 前,我需要做更多的研究。做完这项研究后,我依然不能决定浅的类层次结构在异常处理中是否是个好办法,所以,在此处我给出了两种观点。但是,无论你做什 么,不要抛出new Exception(),不要在需要时继承你自己的异常类。

 

不要把重要的异常信息放在message中

    异常是类。当你返回异常信息时,要创建存储数据的区域。如果你没有这样做,人们为了得到所需要得信息将需要解析Message。现在,想象如果你需要局部 化甚至仅仅想纠正一个错误信息中的拼写错误,会对被调用的代码造成什么影响。你也许永远都不知道你这样做会损坏多少代码。

每个线程要有单独的catch (Exception ex)语句

    在你的应用程序中,普通的异常处理应该被集中解决。每个线程需要一个单独的try/catch模块,否则,你将会丢失异常导致非常难处理的问题的出现。当 一个应用程序启动若干线程去做一些后台处理时,通常你需要创建一个用来存储处理结果的类。不要忘记添加用来存储可能发生的异常的区域,否则在主线程中你将 无法与之通信。在"fire and forget"情况下,你可能需要在线程处理中复制主应用程序异常处理。


一般的异常捕获应该被记录

    你究竟使用什么工具来记录日志——log4net, EIF, Event Log, TraceListeners,text files等等都无关紧要。真正重要的是:如果你捕获一个异常,一定要在某处加以记录。但是仅记录一次——通常代码与记录异常的catch模块一起被丢 掉,然后你以一个庞大的日志结束,此日志拥有太多重复信息。

要记录Exception的全部信息而不仅是Message

    在我们谈论记录日志时,不要忘记你应该经常性的记录Exception.ToString(),而不仅是Exception.Message。 Exception.ToString()将会给你一个堆栈跟踪内部的异常和信息(messae)。通常,这个信息是及其珍贵的,如果你仅记录 Exception.Message,你将会仅仅获得一些诸如“Object reference not set to an instance of an object”的信息。

每个线程只能有一个catch (Exception)语句

    有很少的异常()遵循这一法则。如果你需要捕获一个异常,最好使用你为这段代码编写的最明确的异常类。

    我经常发觉初学者认为好的代码是不抛出异常的代码。这是错误的。好的代码在需要是抛出异常,同时,仅处理那些它知道如何处理的异常。

    作为这个法则的一个应用,请看以下代码。我打赌书写这段代码的人读到这儿的时候想杀我,但是这是一则摘自真实世界的例子。事实上,真实世界的代码要更复杂 一些——我为了说明问题将它大大简化了。第一个类(MyClass)在一个集合,第二个类(GenericLibrary)在另一个集合,这个集合满是普 通代码。在开发机上,这段代码可以正确运行,可是在质量评价(QA)机上,这段代码经常返回“无效数据(Invalid number)”即使输入的数据是有效的。

你能说出为什么会这样吗?

public class MyClass

...{

public static string ValidateNumber(string userInput)

...{

try

...{

int val = GenericLibrary.ConvertToInt(userInput);

return "Valid number";

}

catch (Exception)

...{

return "Invalid number";

}

}

}


public class GenericLibrary

...{

public static int ConvertToInt(string userInput)

...{

return Convert.ToInt32(userInput);

}

}

更多经验

    问题在于过于普通的异常处理者。MSDN的文档中提及,Convert.ToInt32仅仅抛出ArgumentException,FormatException和OverflowException。所以,这些是唯一应该被处理的异常。

    问题在于我们的配置没有包含第二个集合(GenericLibrary)。现在,当我们调用ConvertToInt时就会有一个FileNotFoundException的产生,同时代码假定它是由输入的值无效产生的。

    下一次你书写“catch(Exception ex)”时,尽量描述清楚OutOfMemoryException异常被抛出时,你的代码该如何处理。

不要总是吞掉异常

    你做的最糟糕的事情是在catch (Exception)后加了一个空的模块。永远不要这样做。

 

 

不要把重要的异常信息放在message中

    异常是类。当你返回异常信息时,要创建存储数据的区域。如果你没有这样做,人们为了得到所需要得信息将需要解析Message。现在,想象如果你需要局部 化甚至仅仅想纠正一个错误信息中的拼写错误,会对被调用的代码造成什么影响。你也许永远都不知道你这样做会损坏多少代码。

每个线程要有单独的catch (Exception ex)语句

    在你的应用程序中,普通的异常处理应该被集中解决。每个线程需要一个单独的try/catch模块,否则,你将会丢失异常导致非常难处理的问题的出现。当 一个应用程序启动若干线程去做一些后台处理时,通常你需要创建一个用来存储处理结果的类。不要忘记添加用来存储可能发生的异常的区域,否则在主线程中你将 无法与之通信。在"fire and forget"情况下,你可能需要在线程处理中复制主应用程序异常处理。


一般的异常捕获应该被记录

    你究竟使用什么工具来记录日志——log4net, EIF, Event Log, TraceListeners,text files等等都无关紧要。真正重要的是:如果你捕获一个异常,一定要在某处加以记录。但是仅记录一次——通常代码与记录异常的catch模块一起被丢 掉,然后你以一个庞大的日志结束,此日志拥有太多重复信息。

要记录Exception的全部信息而不仅是Message

    在我们谈论记录日志时,不要忘记你应该经常性的记录Exception.ToString(),而不仅是Exception.Message。 Exception.ToString()将会给你一个堆栈跟踪内部的异常和信息(messae)。通常,这个信息是及其珍贵的,如果你仅记录 Exception.Message,你将会仅仅获得一些诸如“Object reference not set to an instance of an object”的信息。

每个线程只能有一个catch (Exception)语句

    有很少的异常()遵循这一法则。如果你需要捕获一个异常,最好使用你为这段代码编写的最明确的异常类。

    我经常发觉初学者认为好的代码是不抛出异常的代码。这是错误的。好的代码在需要是抛出异常,同时,仅处理那些它知道如何处理的异常。

    作为这个法则的一个应用,请看以下代码。我打赌书写这段代码的人读到这儿的时候想杀我,但是这是一则摘自真实世界的例子。事实上,真实世界的代码要更复杂 一些——我为了说明问题将它大大简化了。第一个类(MyClass)在一个集合,第二个类(GenericLibrary)在另一个集合,这个集合满是普 通代码。在开发机上,这段代码可以正确运行,可是在质量评价(QA)机上,这段代码经常返回“无效数据(Invalid number)”即使输入的数据是有效的。

你能说出为什么会这样吗?

public class MyClass

...{

public static string ValidateNumber(string userInput)

...{

try

...{

int val = GenericLibrary.ConvertToInt(userInput);

return "Valid number";

}

catch (Exception)

...{

return "Invalid number";

}

}

}


public class GenericLibrary

...{

public static int ConvertToInt(string userInput)

...{

return Convert.ToInt32(userInput);

}

}

更多经验

    问题在于过于普通的异常处理者。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模块的最简单的代码:

string ReadTempFile(string FileName)

...{

string fileContents;

using (StreamReader sr = new StreamReader(FileName))

...{

fileContents = sr.ReadToEnd();

}

File.Delete(FileName);

return fileContents;

}

    这段代码在抛出异常时同样遇到一个问题。比如,ReadToEnd函数:它在硬盘上留下临时文件。因此,我真实的看到有人想用如下代码来解决:

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将会阻止资源泄漏即使在有异常出现的地方。

 

 

 

不要在错误条件下返回特殊值

特殊值存在很多问题:

◆异常使得普通的事件更快,因为当你从函数返回特殊值时,每一个函数返回需要被检查,这个过程至少消耗一个进程寄存器或者更多,这些导致了代码的运行缓慢。

◆特殊值可以或者将被忽略。

◆特殊值不携带堆栈追踪,可以丰富错误细节。

◆经常发生的情况是函数没有恰当的可以反映错误情况的值返回。为表示“被清除”这一错误,你该让如

下函数返回什么值呢?

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的一段话:“这个事件仅仅发生在应用程序启动时由系统创建的应用程序领域。如果应用程序创建额外的应用程序领域,在哪些应用程序领域中为这一事件指定代表也是没有作用的。”

◆当代码处理这些事件时,除了异常本身你没有权力使用任何有用信息。你不能关闭数据连接,回滚事务,或其他有用的事情。对初学者来说,使用全局变量的诱惑是巨大的。

    确实,你不应该把你全部的异常处理策略放在这些事件的基础上。想象他们是“安全网“的同时为未来的测试记录异常。之后,确保更正那些没有正确处理异常的代码。

不要重新创造轮子

    有许多很好的框架和库来处理异常。其中的两个是微软提供的,我在这儿介绍以下:

◆异常管理应用模块

◆微软企业使用框架

尽管如此,值得注意的是如果你没有严格的按我所说的原则设计,上述的库则几乎是没用的。

VB.NET

    如果你通读了本篇文章,你将会注意到我在此处书写的所有例子都是C#的。这是因为C#是我首选的语言,而VB.NET本身只有几个指导方针。


仿效C#的“using“陈述

    不幸的是,VB.NET仍然没有using陈述。Whidbey拥有,但是直到它被释放。当你需要处理一个对象的时候,你应该使用如下样式:

Dim sw As StreamWriter = Nothing

Try

sw = New StreamWriter("C:/crivo.txt")

' Do something with sw

Finally

If Not sw is Nothing Then

sw.Dispose()

End if

End Finally

    你调用Dispose时,如果你做一些其他事,可能你就在一些错事,直接导致你的代码出错或是资源泄漏。



不要使用无结构错误处理机制

    无结构错误处理机制同时也被认为是On Error Goto。 1974年,Djikstra教授在撰写“Go To statement considered harmful”时,非常擅长于此。不过那已是三十年前的事了!请尽快从你的应用程序中移除所有无结构错误处理的痕迹。我敢保证On Error Goto语句会对你不利。

结论

    我希望这篇文章可以帮助某些人更好的编码。不仅仅是一些总结出来的经验,我希望这篇文章是讨论如何在我们的代码中使用异常,如何使我们的程序更健壮的起跑点。

    我不认为我所写的这些没有任何错误和有争议的观点。非常乐意听到您关于此话题的意见和建议。

关于作者


    Daniel Turini:十一岁时开始开发软件。在过去的二十年中,他开发软件的同时使用了各种不同的机器和语言,从基于(ZX81,MSX)Z80到大型计算机。 他仍然有研究ASM的激情,虽然从没有使用过它。从专业角度讲,Daniel Turini开发系统来管理大型数据库,这些数据库主要有Sybase 和SQL Server。他所写的大部分方法是面向金融市场,集中于信贷系统。

    迄今为止,Daniel Turini已经学习了大约20种计算机语言。他十分迷恋C#语言和.NET框架,他非常善于做服务器端工作和可重复利用的组件。

 

文章来源:http://hi.baidu.com/wgscd/blog/item/ce651d2f4d0144301f308949.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值