什么时候应该抛出异常
当一个类型的行动成员不能正在完整行动任务时,就应该抛出异常通知调用者。
*行动成员指类型本身或者类型实例可以执行的操作,如C#中StringBuilder中定义的Append,Insert等
捕捉异常代码结构
private void DoSomething()
{
try
{
//将可能发生异常的代码放在这里
}
catch (InvalidOperationException)
{
//捕捉到InvalidOperationException异常,对应的处理代码放在这里
}
catch (IOException)
{
//捕捉到IOException异常,对应的处理代码放在这里
}
catch (Exception)
{
//捕捉到除了上述之外的其它异常,对应的处理代码放在这里
//这里是将异常抛出
throw;
}
finally
{
//这里的代码总是被执行
}
//如果try块没有抛出异常或者某个catch捕捉到异常却没有抛出就执行以下的代码,否则以下代码不执行
}
try块
try块中包含的是可能会发生异常的代码,异常恢复代码应放在一个或多个catch块中。针对应用程序安全的恢复某一种异常都需要有一个对应的catch块。一个try块至少要关联一个catch块或finally块,单独一个try块C#是不允许的,而且这样也没有意义。
*如果一个try块中包含执行多个可能抛出同一个异常类型的操作,但不同的操作对应的恢复措施不同,我们就应该将这些操作拆分到它自己的try块中,以保证正确的恢复状态
catch块
catch块包含的是响应一个异常需要执行的代码。一个try块可以关联0个或多个catch块。如果try块中的代码没有异常发生,CLR永远不会执行catch块中的代码。线程将跳过所有catch块,直至finally块(里面有代码的话)中的代码。catch关键字后面圆括号中的表达式称为捕捉类型,即要捕捉的异常类型。
*使用Visual Studio调试catch块时,可以在监视窗口中添加特殊的变量名称$exception来检查当前抛出异常的对象
catch块检索顺序与注意事项
CLR是自上而下检索一个匹配的catch块,所以编程的时候应注意将派生程度最大的异常类型放在顶部,接着是它们的基类,最后才是System.Exception,如果顺序没有放对,例如将最具体的异常类型放在了最底部的catch块中,C#编译器将会发生错误,因为这个catch块无法被执行到。
一旦try块中的代码发生异常,而没有与之匹配的catch块的话,CLR会去调用栈的更高一层搜索与之匹配的异常类型,如果到了栈的顶部还是没有找到,就会发生一个未处理的异常。
在catch块的末尾可以做的事情
- 重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生
- 抛出一个不同的异常,向调用栈高一层的代码提供更加丰富的异常信息
- 让线程从catch块的底部退出
前两种技术CLR将回溯调用栈,查找捕捉类型与抛出异常的类型匹配的catch块并抛出一个异常。
如果选择让线程从catch块的底部退出,将立即执行包含在finally块中的代码,完毕后执行紧跟在finally块之后的语句。如果不存在finally块,线程将从最后一个catch块之后的语句开始执行。
finally块
finally块中的代码是保证一定会执行的代码且一定要在所有catch块的后面,通常包含的是对try块中的行动所要求的资源清理操作。一个try块最多只能关联一个finally块。
private void ReadFile(string path)
{
FileStream fs = null;
try
{
fs = new FileStream(path, FileMode.Open);
//处理文件数据...
}
catch (IOException)
{
//IOException异常恢复代码
}
finally
{
//确保文件关闭
if (fs != null)
fs.Close();
}
}
上述代码,无论try块代码有没有发生异常,文件都一定会被关闭。如果将关闭文件的代码放在finally块语句之后是不正确的,因为如果抛出异常但没有捕捉到,finally块之后的语句将永远不会被执行,直到下一次垃圾回收才会关闭文件。
*一般情况下catch块和finally块中的代码应只有一两行
System.Exception类属性介绍
- 只读属性Message:String类型,指出抛出异常的原因
- 只读属性Data:IDictionary类型,代码会在抛出异常之前在该集合中添加一个记录项
- 可读可写属性Source:String类型,包含生成异常的程序集的名称
- 只读属性StackTrace:String类型,包含抛出异常之前调用过的所有方法的名称和签名,有助于调试代码
- 只读属性TargetSite:MethodBase类型,包含抛出异常的方法
- 只读属性HelpLink:String类型,包含异常文档的URL,不建议使用
- 只读属性InnerException:Exception类型,通常值为null,如果当前异常是在处理一个异常时抛出的,那么该属性就指出前一个异常是什么
正确使用异常类
- 善用finally块,常用于显示释放对象,避免资源泄露
- 维持状态,发生不可恢复的异常时回滚部分完成的操作
- 在一个线程中捕捉异常,在另一个线程中重新抛出异常
- 合理的从异常中恢复状态
private string SomeMothods()
{
var result = "";
var a = 1;
var b = 0;
try
{
a /= b;
}
catch (DivideByZeroException)
{
result = "被除数不能为0";
}
return result;
}
未处理异常
异常抛出时,CLR会在调用栈中向上查找与抛出异常对象的类型匹配的catch块。如果没有找到,就会发生一个未处理的异常。当CLR检测到进程中的任意一个线程有未处理的异常,都会终止进程。发生未处理异常表明程序遇到了未预料的情况。同时,发生未处理的异常时windows会向事件日志写入一条记录,可以打开事件查看器查看
异常设置
可通过调试菜单打开异常设置窗口如图
展开Common Languages Runtime Exceptions可以查看Visual Studio能够识别的异常类型
如果勾选了异常类型的复选框,调试器就会在抛出该异常的时候中断,此时CLR不会查找任何与之匹配的catch块。
如果异常类的复选框没有勾选,调试器只有在该异常类型未得到处理时才会中断。
通过添加操作还可以添加自定义的异常类型
异常处理的性能问题
- 非托管c++编译器:必须生成代码来跟踪哪些对象被构造成功,编译器还必须生成代码用来在一个异常被捕捉到的时候,调用每一个已经成功构造的对象的析构器。如此便会在程序中生成大量的bookkeeping代码,对代码的大小和执行时间都会造成负面影响。
- 托管编译器:由于托管对象是在托管堆中分配的,而托管堆受到垃圾回收的监视。如果一个对象成功构造并且抛出一个异常,垃圾回收器最终会释放对象的内存。编译器无需生成任何bookkeeping代码来跟踪成功构造的对象,也无需保证析构器的调用。与托管c++相比,意味着生成的代码更少,运行要执行的代码更少,应用程序的性能更好。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。