C#异常处理trycatch
原⽂地址:点击打开链接
本⽂翻译⾃CodeProject上的⼀篇⽂章,原⽂地址。
⽬录
介绍
做最坏的打算
提前检查
不要信任外部数据
可信任的设备:摄像头、⿏标以及键盘
“写操作”同样可能失效
安全编程
不要抛出“new Exception()”
不要将重要的异常信息存储在Message属性中
每个线程要包含⼀个try/catch块
捕获异常后要记录下来
不要只记录Exception.Message的值,还需要记录Exception.ToString()
要捕获具体的异常
不要中⽌异常上抛
清理代码要放在finally块中
不要忘记使⽤using
不要使⽤特殊返回值去表⽰⽅法中发⽣的异常
不要使⽤“抛出异常”的⽅式去表⽰资源不存在
不要将“抛出异常”作为函数执⾏结果的⼀种
可以使⽤“抛出异常”的⽅式去着重说明不能被忽略的错误
不要清空了堆栈跟踪(stack trace)信息
异常类应标记为Serializable
使⽤”抛出异常”代替Debug.Assert
每个异常类⾄少包含三个构造⽅法
不要重复造轮⼦
VB.NET
模拟C#中的using语句
不要使⽤⾮结构化异常处理(On Error goto)
总结
介绍
“我的软件程序从来都不会出错”。你们相信吗?我⼏乎可以肯定所有⼈都会⼤喊我是个骗⼦。“软件程序⼏乎不可能没有bug!”
事实上,开发⼀个可信任、健全的软件程序并不是不可能的事情。注意我这⾥并不是指那些⽤于控制核电站的软件,⽽是指⼀些常见的商业软件,这些软件可能运⾏在服务器上,⼜或者PC机上,它们可以连续⼯作⼏个星期甚⾄⼏个⽉都不会出现重⼤问题。可以猜到,我刚才的意思是指软件有⼀个⽐较低的出错率,你可以迅速到出错的原因并快速修复,并且出现的错误并不会造成重⼤的数据损坏。
换句话说,我的意思是指软件⽐较稳定。
软件中有bug是可以理解的。但是如果是经常出现的bug,并且因为没有⾜够的提⽰信息导致你不能迅速修复它,那么这种情况是不可被原谅的。
为了更好地理解我上⾯所说的话,我举个例⼦:我经常看见⽆数的商业软件在遇到硬盘不⾜时给出这样的错误提⽰:
“更新客户资料失败,请与系统管理员联系然后重试”。
除了这些外,其他任何信息都没有被记录。要搞清楚到底什么原因引起的这个错误是⼀件⾮常耗时的过程,在真正到问题原因之前,程序员可能需要做各种各样的猜测。
注意在这篇⽂章中,我主要讲怎样更好地处理.NET编程中的异常,并没有打算讨论怎样显⽰合适的“错误提⽰信息”,因为我觉得这个⼯作属于UI界⾯开发者,并且它⼤部分依赖于UI界⾯类型以及最终使⽤软件的⽤户。⽐如⼀个⾯向普通⽤户的⽂本编辑器的“错误提⽰信息”应该完全不同于⼀个Socket通信框架,因为后者直接⽤户是程序员。
做最坏的打算
遵守⼀些基本的设计原则可以让你的程序更加健全,并且当错误发⽣时,能够提升⽤户体验。我这⾥说到的“提升⽤户体验”并不是指错误的提⽰窗体能够让⽤户⾼兴,⽽是指发⽣的错误不会损坏原有数据,不会让整个电脑崩溃。如果你的程序遇到硬盘不⾜的错误,但是程序不会造成其他任何负⾯效果(仅仅提⽰错误信息,不会引起其他问题,译者注),那么这时候就提升了⽤户体验。
提前检查
强类型检查和验证是避免bug发⽣的有⼒⽅法。你越早发现问题,就越早修复问题。⼏个⽉后再想搞清楚“为什么InvoiceItems表中的ProductID栏会存在⼀个CustomerID数据?”是⼀件不太容易并且相当恼
⽕的事情。如果你使⽤⼀个类代替基本类型(如int、string)去存储客户(Customer)的数据的话,编译器就不会允许刚才那件事情(指将CustomerID和ProductID混淆,译者注)发⽣。
不要信任外部数据
外部数据是不可靠的,我们的软件程序在使⽤它们之前必须严格检查。⽆论这些外部数据来⾃于注册表、数据库、硬盘、socket还是你⽤键盘编写的⽂件,所有这些外部数据在使⽤前必须严格进⾏检查。很多时候,我看到⼀些程序完全信任配置⽂件,因为开发这些程序的程序员总是认为没有⼈会编辑配置⽂件并损坏它。
可信任的设备:摄像头、⿏标以及键盘
当你需要⽤到外部数据时,你可能会遇到以下情况:
  1)没有⾜够的安全权限
  2)数据不存在
  3)数据不完整
  4)数据完整,但是格式不对
不管数据源是注册表中的某个键、⼀个⽂件、socket套接字、数据库、Web服务或者串⼝,以上情况均可能发⽣。所有的外部数据总会有失效的可能。
“写操作”同样可能失效
不可信任的数据源同样也是⼀种不可信任的数据仓库。当你存储数据时,相似情况依旧可能会发⽣:
  1)没有⾜够的安全权限
  2)设备不存在
  3)没有⾜够的空间
  4)存储设备发⽣了物理错误
这就是为什么⼀些压缩软件在⼯作时创建了⼀个临时⽂件,当⼯作完成后再重命名,⽽不是直接修改源⽂件。原因是如果硬盘损坏(或者软件异常)可能导致原始数据丢失。(译者遇见过这种情况,备份数据时断电,结果原来的旧版备份被损坏了,译者注)
安全编程
我的⼀个朋友告诉我:⼀个好的程序员从来不会在他的程序中编写糟糕的代码。我觉得这只是成为⼀个好程序员的必要条件⽽不是充分条件。下⾯我整理了⼀些当你进⾏异常处理时,可能会编写的“糟糕代码”:
不要抛出“new Exception()”
请别这样做。Exception是⼀个⾮常抽象的异常类,捕获这类异常通常会产⽣很多负⾯影响。通常情况下应该定义我们⾃⼰的异常类,并且需要区分系统(framework)抛出的异常和我们⾃⼰抛出的异常。
不要将重要的异常信息存储在Message属性中
异常都封装在类中。当你需要返回异常信息时,请将信息存储在⼀些单独的属性中(⽽不要放在Message属性中),否则⼈们很难从Message属性中解析出他们需要的信息。⽐如当你仅仅需要纠正⼀下拼写错误,如果你将错误信息和其它提⽰内容⼀起以String的形式写在了Message属性中,那么别⼈该怎样简单地获取他们要的错误信息呢?你很难想象到他们要做多少努⼒。
每个线程要包含⼀个try/catch块
⼀般异常处理都放在了程序中⼀个⽐较集中的地⽅。每个线程都需要有⼀个try/catch块,否则你会漏掉某些异常从⽽出现难以理解的问题。当⼀个程序开启了多个线程去处理后台任务时,通常你会创建
⼀个类型来存储各个线程执⾏的结果。这时候请不要忘记了为类型增加⼀个字段来存储每个线程可能发⽣的异常,否则的话,主线程不会知道其他线程的异常情况。在⼀些“即发即忘”的场合(意思主线程开启线程后不再关⼼线程的运⾏情况,译者注),你可能需要将主线程中的异常处理逻辑复制⼀份到你的⼦线程中去。
捕获异常后要记录下来
不管你的程序是使⽤何种⽅式记录⽇志——log4net、EIF、Event Log、TraceListeners或者⽂本⽂件等,这些都不重要。重要的是:当你遇到异常后,应该在某个地⽅将它记录在⽇志中。但是请仅仅记录⼀次,否则的话,你最后会得到⼀个⾮常⼤的⽇志⽂件,包含了许多重复信息。try catch的使用方法
不要只记录Exception.Message的值,还需要记录Exception.ToString()
当我们谈到记录⽇志时,不要忘了我们应该记录Exception.ToString()的值,⽽不是Exception.Message。因为Exception.ToString()包含了“堆栈跟踪”(stack trace)信息,内部异常信息以及Message。通常这些信息⾮常重要,⽽如果你只记录Exception.Message的话,你只可能看到类似“对象引⽤未指向堆中实例”这样的提⽰。
要捕获具体的异常
如果你要捕获异常,请尽可能的捕获具体异常(⽽⾮Exception)。
我经常看见初学者说,⼀段好的代码就是不能抛出异常的代码。其实这说法是错误的,好的代码在必要时应该抛出相应的异常,并且好的代码只能捕获它知道该怎么处理的异常(注意这句话,译者注)。
下⾯的代码作为对这条规则的说明。我敢打赌编写下⾯这段代码的那个家伙看见了会杀了我的,但是它确实是摘取⾃真实编程⼯作中的⼀段代码。
第⼀个类MyClass在⼀个程序集中,第⼆个类GenericLibrary在另⼀个程序集中。在开发的机器上运⾏正常,但是在测试机器上却总是抛出“数据不合法!”的异常,尽管每次输⼊的数据都是合法的。
你们能说说这是为什么吗?
[csharp] view plain copy
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.dll)打包进去。所以
程序运⾏后,ConvertToInt⽅法会抛出FileNotFoundException异常,但是我们捕获的异常是Exception,所以会提⽰“数据不合法”。
不要中⽌异常上抛
最坏的情况是,你编写catch(Exception)这样的代码,并且在catch块中啥也不⼲。请不要这样做。
清理代码要放在finally块中
⼤多数时候,我们只处理某⼀些特定的异常,其它异常不负责处理。那么我们的代码中就应该多⼀些finally块(就算发⽣了不处理的异常,也可以在finally块中做⼀些事情,译者注),⽐如清理资源的代码、关闭流或者回复状态等。请把这当作习惯。
有⼀件⼤家容易忽略的事情是:怎样让我们的try/catch块同时具备易读性和健壮性。举个例⼦,假设你需要从⼀个临时⽂件中读取数据并且返回⼀个字符串。⽆论什么情况发⽣,我们都得删除这个临时⽂件,因为它是临时性的。
让我们先看看最简单的不使⽤try/catch块的代码:
[csharp] view plain copy
string ReadTempFile(string FileName)
{
string fileContents;
using (StreamReader sr = new StreamReader(FileName))
{
fileContents = sr.ReadToEnd();
}
File.Delete(FileName);
return fileContents;
}
这段代码有⼀个问题,ReadToEnd⽅法有可能抛出异常,那么临时⽂件就⽆法删除了。所以有些⼈修改代码为:
[csharp] view plain copy
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的⽅式:
[csharp] view plain copy
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;
}
}
变量fileContents去哪⾥了?它不再需要了,因为返回点在清理代码前⾯。这是让代码在⽅法返回后才执⾏的好处:你可以清理那些返回语句需要⽤到的资源(⽅法返回时需要⽤到的资源,所以资源只能在⽅法返回后才能释放,译者注)。
不要忘记使⽤using
仅仅调⽤对象的Dispose()⽅法是不够的。即使异常发⽣时,using关键字也能够防⽌资源泄漏。(关于
对象的Dispose()⽅法的⽤法,可以关注我的书,有⼀章专门介绍。译者注)
不要使⽤特殊返回值去表⽰⽅法中发⽣的异常
因为这样做有很多问题:
  1)直接抛出异常更快,因为使⽤特殊的返回值表⽰异常时,我们每次调⽤完⽅法时,都需要去检查返回结果,并且这⾄少要多占⽤⼀个寄存器。降低代码运⾏速度。
  2)特殊返回值能,并且很可能被忽略
  3)特殊返回值不能包含堆栈跟踪(stack trace)信息,不能返回异常的详细信息
  4)很多时候,不存在⼀个特殊值去表⽰⽅法中发⽣的异常,⽐如,除数为零的情况:
[csharp] view plain copy
public int divide(int x, int y)
{
return x / y;
}
不要使⽤“抛出异常”的⽅式去表⽰资源不存在
微软建议在某些特定场合,⽅法可以通过返回⼀些特定值来表⽰⽅法在执⾏过程中发⽣了预计之外的事情。我知道我上⾯提到的规则恰恰跟这条建议相反,我也不喜欢这样搞。但是⼀些API确实使⽤了某些特殊返回值来表⽰⽅法中的异常,并且⼯作得很好,所以我还是觉得你们可以谨慎地遵循这条建议。
我看到了.NET Framework中很多获取资源的API⽅法使⽤了特殊返回值,⽐如Assembly.GetManifestStream⽅法,当不到资源时(异常),它会返回null(不会抛出异常)。
不要将“抛出异常”作为函数执⾏结果的⼀种
这是⼀个⾮常糟糕的设计。代码中包含太多的try/catch块会使代码难以理解,恰当的设计完全可以满⾜⼀个⽅法返回各种不同的执⾏结果(绝不可能到了必须使⽤抛出异常的⽅式才能说明⽅法执⾏结果的地步,译者注),如果你确实需要通过抛出异常来表⽰⽅法的执⾏结果,那只能说明你这个⽅法做了太多事情,必须进⾏拆分。(这⾥原⽂的意思是,除⾮确实有异常发⽣,否则⼀个⽅法不应该仅仅是为了说明执⾏结果⽽抛出异常,也就是说,不能⽆病呻呤,译者注)

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。