C# 异常处理

异常

程序运行过程中,可能会遇到各种情况问题,叫做异常.异常如果不进行处理,会导致程序结束.

产生异常

int value = Int32.Parse("ab");

由于输入的字符串格式错误,不能转换成整数

程序会报FormatException,然后退出.

结构化异常处理语法

try{
    可能出异常的语句
}
catch(异常类型1 变量名){
    处理异常
}
...
catch(异常类型2 变量名){
    处理异常
}
finally{
    出错与否都执行的语句
}

catch块可以由0个或多个.

catch和finally两者必须有一个,也可以都有.

多个catch语句中的异常类型不能相同,而且抽象的类型要放在下面.

finally语句通常用来清理资源.

finally语句中的代码不会被ThreadAbortException

处理异常

try
{
    int value = int32.Parse("a12");
}
catch(FormatException)
{
    Console.WriteLine("格式错误");
}

异常类

在C#中,所有的异常都继承自Exception类.

常见异常

异常类型 说明 产生原因 不应自行抛出
ArgumentNullException 向不可为空的参数传递了null 缺少检查 开发时
NullReferenceException 使用了引用为null的对象 缺少检查 开发时
StackOverflowException 栈溢出 递归过深 开发时
FormatException 字符串格式错误 用户输入错误 运行时
OutOfMemoryException 内存不足 运行时
IOException 读写错误 运行时
ThreadAbortException 线程中断 调用Thread.Abort()后系统抛出 运行时
IndexOutOfRangeException 索引超过边界 缺少检查 开发时
ArgumentException 参数错误 缺少检查 开发时

开发时异常是开发人员缺少检查,是开发人员的错误,开发时即可发现,不建议捕获.

运行时异常是是不可预料的,受环境影响.

部分异常不应自行抛出,应为他们有特殊含义,只应由系统抛出.

抛出异常

语法

throw [异常变量];

如果在catch语句块中,异常变量可以省略,表示再次抛出异常并保留当前堆栈信息.

抛出异常

int Sum(int[] array){
    if(array = null) throw new ArgumentNullException(nameof(array));
    int sum = 0;
    foreach(var item in array){
        sum += item;
    }
    return sum;
}

上面的代码要求参数不能为null.

定义自己的异常

如果没有合适的异常,可以定义自己的异常.

异常必须继承自Exception

异常类型应以Exception结尾

异常应为公共的(不是公共的调用者只能以Exception类型捕获)

异常应实现Exception类的所有构造函数

异常应实现可序列化.

提示:使用Visual Studio的Exception代码段即可实现符合要求的异常类型.

    [Serializable]
    public class MyException : Exception
    {
        public MyException() { }
        public MyException(string message) : base(message) { }
        public MyException(string message, Exception inner) : base(message, inner) { }
        protected MyException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
    }

异常处理规则

不要捕获Exception类型异常,因为不可能处理所有异常,比如栈溢出是不能进行恢复的,开发时异常也不应该捕获.

一定要捕获Exception类型异常的话,一定要再次抛出来,不要吃掉.

越底层的(远离用户)的代码越大胆抛出异常,越高层(接近用户)的代码越大胆捕获异常.

调用方法前尽可能先检查.比如读文件前先检查文件是否存在,使用TryParse替代Parse.

开发时异常尽量没有.

不要抛出Exception类型异常,这样使用者无法的得知异常类型,无法进行处理

抛出异常时优先使用系统已有异常

合理定义自己的异常类

为何使用结构化异常处理

基本上,每个函数都有可能产生异常,从参数错误,到各种环境产生的错误.而且函数本身也存在互相调用,导致错误可能产生在任何地方.

如果没有异常处理,每个函数都应该带有一个额外的返回值,表示错误信息,每次调用后都需要检查,这对于编码是十分不友好的.

结构化异常处理使得程序在产生异常时,一直向上找调用者,直到找到可以处理此异常的代码,错误产生者和处理者只需要共同基于异常类型进行约定,而不要在乎调用层次.

结构化异常处理使用性能上的小损失,带来开发上的效率.

using语句和异常处理

using语句可以在结束时清理资源,内部使用了try ... finally

语法

using(变量1,变量2){

}

变量类型要相同

变量需要实现IDisposable

using语句

using(FileStream fs = File.OpenRead(@"file.txt")){

}

内部实现

FileStream fs = null;
try
{
    fs = File.OpenRead(@"file.txt");
}
finally
{
    if(fs != null){
        fs.Dispose();
    }
}

lock语句块和异常处理

lock语句块用于获取对象的排他锁,内部使用了try ... finally

lock(obj){
    语句块
}

内部实现

try
{
    Monitor.Enter(obj);
    语句块
}
finally
{
    Monitor.Exit(obj);
}

异常过滤器

C#6.0版本后,支持异常过滤器

语法

try
{
    语句
}
catch(异常类型 变量) when(表达式)
{
    处理语句
}
catch(异常类型 变量) when(表达式)
{
    处理语句
}

通过异常表达式,可以进一步限定捕获的范围.

在有when表达式的情况下,下面的catch语句中的异常类型可以比上面的更具体.