.NET 异常处理
(好久没写过技术文章了,之前遭受了严重的打击,曾经沮丧不已;现已慢慢恢复过来^_^,因此不能荒废了自己的技术。)
没有不出错的软件
从不出错的软件从某种程度上讲是不可能的!
和普通人的观念相反,创造可信赖的,健壮的软件并不是一件不可能的事情。大部分的商业软件,在长时间可以无重大故障的工作,但它们并非没有任何的错误,只是拥有低出错率,你可以迅速理解出错原因然后快速搞定它,并且,它不会因为外部错误而毁坏数据。软件中有错误是可以原谅的,甚至是可以预料到的;不可原谅的是您无法解决一个复发的错误,仅仅是因为您没用足够的信息。
另外,如果希望成功避免发生错误,就必须要求开发人员正确对待错误。
做好最坏的打算
所有代码的书写,你应该认为,它是会发生错误的。如果太过于相信自己,往往会导致错误没有被处理,因此也就无法或者非常困难得去获得错误的详细信息。
不要相信外部数据
外部数据是不可相信的。不管这些数据是来自寄存器、数据库、硬盘、socket、文件或者是键盘,它们都必须被检查,只有这样你才可以去使用它们。
唯一可信赖的外部设施是:显示器、鼠标和键盘。
不要理所当然
举个例子,你打开一个TXT文件并修改后,希望原地保存,因此往往理所当然地认为,能打开即能保存。然而,该文件可能是只读的,又或者所在驱动器只读,甚至磁盘空间不够。如果你理所当然,那程序也就理所当然地出错了。
合理使用.NET异常处理机制
当你摆正观念以后,就可以使用.NET完善的异常处理机制,来抒写安全的代码。事实上,并非掌握了try{}catch{}语法后,就能抒写安全的代码。其中非常多需要注意的事项。什么时候try?怎么try?怎么catch,都是可以商酌的。我这里介绍的仅仅是比较好的方法。
不要抛出new Exception()
Exception是一个非常大的类,如果没有side-effect(副作用),很难去捕获。引用你自己的异常类,但是使它继承自AppliationException。通过这种方法,你可以设计一个专门的异常捕获程序去捕获框架抛出的异常,同时设计另一个异常捕获程序来处理自己抛出的异常。
每个线程要有单独的catch (Exception ex)语句
在你的应用程序中,普通的异常处理应该被集中解决。每个线程需要一个单独的try/catch模块,否则,你将会丢失异常导致非常难处理的问题的出现。当一个应用程序启动若干线程去做一些后台处理时,通常你需要创建一个用来存储处理结果的类。不要忘记添加用来存储可能发生的异常的区域,否则在主线程中你将无法与之通信。
要记录Exception的全部信息
在捕获错误以后,应该记录的是Exception.ToString(),而不仅是Exception.Message。Exception.ToString()将会给你一个堆栈跟踪内部的异常和信息。通常,这个信息是及其珍贵的,如果你仅记录Exception.Message,你将会仅仅获得一些没用的信息。
要有清理代码
不要忽略的一件事是try/finally 模块如何使你的代码变得更加可读与健壮,这是处理代码的巨大作用所在。
举个例子,假设你需要从一个临时文件中阅读一些临时信息,然后以字符串的形式返回它。不管发生什么,你都必须删除这一文件。这样的返回处理功能需要try/finally模块来完成。
try
{
string fileContents;
using (StreamReader sr = new StreamReader(FileName))
{
fileContents = sr.ReadToEnd();
}
File.Delete(FileName);
return fileContents;
}
catch (Exception ex)
{
//错误处理
}
finally
{
File.Delete(FileName);
}
经常使用using
仅仅在一个对象上调用Dispose()函数是远远不够的。关键字using将会阻止资源泄漏即使在有异常出现的地方。从上面的例子我们也可以看到这个好处。
当再次抛出异常时不要清空堆栈追踪
堆栈追踪是一个异常携带的最有用的信息之一。经常,我们需要在catch模块中,放入一些异常处理代码(如,回滚一个事务)然后再抛出异常。但很多人会犯下以下的错误:
try
{
// 一些代码
}
catch (Exception ex)
{
// 写日志
throw ex;
}
为什么这个是错误的呢?因为,当你检查堆栈跟踪时,异常将会运行到“throw ex”这一行,隐藏了真实的出错位置。你可以试一下。
try
{
//…
}
catch (Exception ex)
{
//…
throw;
}
观察以上代码什么改变了呢?取代了这个将会抛出新异常同时清空堆栈追踪的“throw ex;”语句,我们使用了简单的“throw;”语句。如果你没有指定这个异常,throw 声明将会仅仅再次抛出catch声明捕获的异常。这将会保证你的堆栈追踪完整无缺,但是依然允许你在catch模块中放入代码。
全局错误捕获
尽管你已经在极大部分的代码里面加入try{}catch{}错误捕获语句,但依然无法肯定你的代码不会有未捕获的错误。允许未捕获错误的发生,会直接让程序非法操作,并且无法获取该错误的详细信息。
全局错误Application.ThreadException的捕获能帮你解决这个问题。首先,我们先建立一个用户错误处理类:
/// <summary>
/// 用户错误处理类
/// </summary>
public static class UserException
{
/// <summary>
/// 全局未捕获错误处理函数
/// </summary>
public static void ThreadException(object sender,
System.Threading.ThreadExceptionEventArgs e)
{
Exception ex = e.Exception;
//写日志
Application.Exit();
}
/// <summary>
/// 全局错误处理函数
/// </summary>
public static void DoException(Exception ex)
{
//写日志
}
}
然后在应用程序Program类中,添加事件委托:
//委托错误处理事件
Application.ThreadException += new
System.Threading.ThreadExceptionEventHandler(
UserException.ThreadException);
事实上,错误捕获以后,应用程序应该关闭,而不应该继续运行。但是添加全局错误捕获以后,可以记录下未知错误的详细信息,甚至做更多的事情,例如:错误发送。
应用程序域级别的错误捕获
事实上,Application.ThreadException不是万能的,最重要的一点,它不能捕获线程级的错误。
这里介绍另外一种错误捕获AppDomain. UnhandledException方法,它是基于应用程序域(一种边界,它由公共语言运行库围绕同一应用程序范围内创建的对象建立。应用程序域有助于将在一个应用程序中创建的对象与在其他应用程序中创建的对象隔离,以使运行时行为可以预知。在一个单独的进程中可以存在多个应用程序域。)级别的。
然而必须告诉你的是,这个方法并不建议用于捕获线程级的错误。理由有很多,其中一个是,此方法捕获错误后,程序必须退出,因此无法实现某些中断线程而主进程继续允许的需求。
同样我们需要建立一个应用程序错误处理类:
/// <summary>
/// 应用程序错误处理类
/// </summary>
public static class AppException
{
/// <summary>
/// 整个应用程序域中未捕获错误处理函数
/// </summary>
public static void DomainUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
Exception ex = (Exception)args.ExceptionObject;
//应用程序必须关闭,只能提前写写日志了
Application.Exit();
Environment.Exit(0);
}
}
然后在应用程序Program类中,添加事件委托:
//委托错误处理事件
AppDomain.CurrentDomain.UnhandledException += new
UnhandledExceptionEventHandler(
AppException.DomainUnhandledException);
必须记住的是,捕获应用程序域的错误以后,程序必须用语句关闭,否则程序依然会发生恐怖非法操作。
线程级的错误捕获
你会发现,开了多线程的程序,发生错误的几率会大大增加。但是有时候,并不能因为某个线程发生了错误,而必须终止进程。另外,线程中try…catch以后,如果和主进程交互,都是个困难的问题。
不管出于怎样的考虑,都建议把每个线程中捕获的错误,放到一个专门的类中去处理。如果需要与主进程交互,这个类还应该有委托函数,把处理交给主进程。
/// <summary>
/// 线程错误处理类
/// </summary>
public class WorkerThreadException
{
public WorkerThreadException()
{
}
public WorkerThreadException(Exception ex)
{
new WorkerThreadExceptionHandlerDelegate(
WorkerThreadExceptionHandler).BeginInvoke(
ex, null, null);
}
/// <summary>
/// 工作线程错误捕获委托
/// </summary>
public delegate void WorkerThreadExceptionHandlerDelegate(
Exception ex);
/// <summary>
/// 工作线程错误捕获处理函数
/// </summary>
public void WorkerThreadExceptionHandler(Exception ex)
{
AppException.UnhandledException(
this,
new System.Threading.ThreadExceptionEventArgs(ex));
}
}
可以看到,该类把捕获到的错误,引发到Application.ThreadException中,统一处理。当然,你可以做更多的事情。下面,我们创建一个线程:
private void button_Click(object sender, EventArgs e)
{
//使用独立线程引发错误
Thread thread = new Thread(new ThreadStart(TestError));
thread.Start();
}
private void TestError()
{
try
{
//除以零
int i = 0;
i = 4 / i;
}
catch (Exception ex)
{
new WorkerThreadException(ex);
}
}
可以看到,线程中catch到错误后,创建了WorkerThreadException类,由该类统一处理,而不是在线程中抒写重复的代码。无论从代码、还是交互角度来看,都建议这样做。
最后
此文并没有介绍调试技巧,然而预防胜于补漏,合理利用.NET的异常处理机制,就能很好的捕获到错误及其信息,减少调试的时间。
最后想说明的是,发生错误并不可怕;可怕的是,错误重复发生,而你却对它一无所知。