1什么是异常
异常就是程序运行期间出现的问题。先看一个什么都没处理的例子:25 除以 0。
| 11 | public void division(int num1, int num2) | // division 方法:做除法 |
| 12 | { | // division 开始 |
| 13 | result = num1 / num2; | // 25 / 0:整数除以零,在这里出事 |
| 14 | Console.WriteLine("Result: {0}", result); | // 这一行根本执行不到 |
| 15 | | |
| 16 | } | // division 结束 |
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero. at ErrorHandlingApplication.DivNumbers.division(Int32 num1, Int32 num2) in 异常处理/nocatch/Program.cs:line 13 at ErrorHandlingApplication.DivNumbers.Main(String[] args) in 异常处理/nocatch/Program.cs:line 20
程序直接崩溃:第一行写着“未处理的异常”(Unhandled exception),类型是 DivideByZeroException。异常处理就是把控制权从出事的地方,转到专门处理它的地方。
视频里用分拣台演示:后排轨道是 try 段,前排每道闸口是一个 catch,右边的盖章站是 finally;出事时小车挂上红牌(throw,抛出异常),所有闸口都不认,小车就掉进坑里,程序终止。真实程序里没有小车,它只是帮你看清控制权跳到了哪里。
2try、catch、finally
try { }:放可能出事的语句。catch (异常类型 e) { }:可以排好几个,每个接一种异常。finally { }:放无论如何都要执行的语句。
try 后面可以跟一个或多个 catch;也可以不写 catch,只跟一个 finally,这叫 try-finally 语句。
| 9 | Console.WriteLine(scores[3]); | // try 里:scores 只有 3 个元素(下标 0~2),读下标 3 越界 |
| 10 | } | // try 结束 |
| 11 | catch (DivideByZeroException e) | // 第一道闸口:只接除以零,不认,继续往下比 |
| 12 | { | // 第一个 catch 开始 |
| 13 | Console.WriteLine("除以零:" + e.Message); | // 不执行 |
| 14 | } | // 第一个 catch 结束 |
| 15 | catch (IndexOutOfRangeException e) | // 第二道闸口:接下标越界,对上了 |
| 16 | { | // 第二个 catch 开始 |
| 17 | Console.WriteLine("下标越界:" + e.Message); | // 打印提示和异常自带的消息 e.Message |
| 18 | } | // 第二个 catch 结束 |
后面还跟着一个 finally,打印“finally 执行了”:
下标越界:Index was outside the bounds of the array. finally 执行了
catch 从上往下一个个比,最多只执行一个。把能接住一切的 Exception 放在最前面,后面那个永远轮不到,编译器直接报错:
| 11 | catch (Exception e) | // 笼统的 Exception 写在前面,什么异常都接 |
| 12 | { | // 第一个 catch 开始 |
| 13 | Console.WriteLine("出错了:" + e.Message); | // 打印消息 |
| 14 | } | // 第一个 catch 结束 |
| 15 | catch (IndexOutOfRangeException e) | // 具体的写在后面:永远轮不到 |
Program.cs(15,14): error CS0160: 上一个 catch 子句已经捕获了此类型或超类型(“Exception”)的所有异常
所以 catch 要先写具体的,再写笼统的。
3异常类
C# 里每一种异常都是一个类,都直接或间接派生自 System.。下面有两个分支:ApplicationException 原本留给程序员自己定义异常用,SystemException 装的是 .NET 预先定义好的异常。也有预定义异常直接派生自 Exception,比如网络请求出错时的 HttpRequestException:
| 6 | Type t = typeof(System.Net.Http.HttpRequestException); | // typeof 取到这个异常类的类型信息 |
| 7 | Console.WriteLine(t.Name + " → " + t.BaseType?.Name); | // 打印类名和它的父类名(?. 表示父类为空就不往下取) |
| 8 | t = typeof(AggregateException); | // 再看 AggregateException |
| 9 | Console.WriteLine(t.Name + " → " + t.BaseType?.Name); | // 打印类名和父类名 |
HttpRequestException → Exception AggregateException → Exception
八种预定义异常
IOException:读写文件、流这类输入输出出错。IndexOutOfRangeException:数组下标超出范围。ArrayTypeMismatchException:往数组里放了类型不匹配的元素。NullReferenceException:变量是空值,却去用它的成员。DivideByZeroException:整数除以零。InvalidCastException:强制类型转换失败。OutOfMemoryException:内存不够了。StackOverflowException:栈溢出,常见于方法没完没了地调用自己。
前七种都真的触发了一遍。Try 是程序里的辅助方法:运行传进来的一段代码,接住异常后打印它的类型名;() => 是 lambda 写法,把后面这段代码打包交给 Try。
| 17 | Try(() => new MemoryStream().Seek(-1, SeekOrigin.Begin)); | // 读写位置移到开头前面:IOException |
| 18 | Try(() => arr[3] = 1); | // arr 只有 3 个元素,写下标 3 |
| 19 | Try(() => objs[0] = 1); | // objs 实际是 string 数组,放进 int |
| 20 | Try(() => Console.WriteLine(s.Length)); | // s 是 null,去取 Length |
| 21 | Try(() => Console.WriteLine(10 / zero)); | // zero 的值是 0:10 / 0 |
| 22 | Try(() => Console.WriteLine((int)o)); | // o 里装的是字符串 "abc",硬转成 int |
| 23 | Try(() => Console.WriteLine(new int[int.MaxValue].Length)); | // 一下子申请超大数组 |
IOException IndexOutOfRangeException ArrayTypeMismatchException NullReferenceException DivideByZeroException InvalidCastException OutOfMemoryException
输出按顺序对应表里前七行。栈(存放方法调用记录的一块内存)溢出没法这样演示:它根本接不住,程序会直接结束。这些异常类只是表示某种错误,真正处理它的是你写的 catch。
家谱
IOException → SystemException → Exception → Object IndexOutOfRangeException → SystemException → Exception → Object ArrayTypeMismatchException → SystemException → Exception → Object NullReferenceException → SystemException → Exception → Object DivideByZeroException → ArithmeticException → SystemException → Exception → Object InvalidCastException → SystemException → Exception → Object OutOfMemoryException → SystemException → Exception → Object StackOverflowException → SystemException → Exception → Object ApplicationException → Exception → Object
大部分直接派生自 SystemException,DivideByZeroException 中间还隔着一层 ArithmeticException。顺着往上全都通到 Exception,所以 catch ( 能接住一切。
4例子:接住除以零
| 15 | result = num1 / num2; | // try 里的除法:25 / 0 出事,result 还是 0 |
| 16 | } | // try 结束 |
| 17 | catch (DivideByZeroException e) | // 接住除以零的异常 |
| 18 | { | // catch 开始 |
| 19 | Console.WriteLine("Exception caught: {0}", e); | // 直接打印 e:类型、消息,后面还带一长串堆栈信息 |
| 20 | } | // catch 结束 |
| 21 | finally | // 不管出没出事都执行 |
| 22 | { | // finally 开始 |
| 23 | Console.WriteLine("Result: {0}", result); | // 打印 Result: 0 |
查看完整代码(34 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace ErrorHandlingApplication | // 定义命名空间 |
| 3 | { | // 命名空间开始 |
| 4 | class DivNumbers | // 定义 DivNumbers 类 |
| 5 | { | // DivNumbers 类开始 |
| 6 | int result; | // 字段 result:存除法结果 |
| 7 | DivNumbers() | // 构造函数 |
| 8 | { | // 构造函数开始 |
| 9 | result = 0; | // result 初值 0 |
| 10 | } | // 构造函数结束 |
| 11 | public void division(int num1, int num2) | // division 方法:num1 除以 num2 |
| 12 | { | // division 开始 |
| 13 | try | // 可能出事的语句放进 try |
| 14 | { | // try 开始 |
| 15 | result = num1 / num2; | // 做除法 |
| 16 | } | // try 结束 |
| 17 | catch (DivideByZeroException e) | // 接住除以零 |
| 18 | { | // catch 开始 |
| 19 | Console.WriteLine("Exception caught: {0}", e); | // 打印异常 |
| 20 | } | // catch 结束 |
| 21 | finally | // 无论如何都执行 |
| 22 | { | // finally 开始 |
| 23 | Console.WriteLine("Result: {0}", result); | // 打印结果 |
| 24 | } | // finally 结束 |
| 25 | | |
| 26 | } | // division 结束 |
| 27 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 28 | { | // Main 开始 |
| 29 | DivNumbers d = new DivNumbers(); | // 创建 DivNumbers 对象 |
| 30 | d.division(25, 0); | // 25 除以 0 |
| 31 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 32 | } | // Main 结束 |
| 33 | } | // DivNumbers 类结束 |
| 34 | } | // 命名空间结束 |
Exception caught: System.DivideByZeroException: Attempted to divide by zero. at ErrorHandlingApplication.DivNumbers.division(Int32 num1, Int32 num2) in 异常处理/ex1/Program.cs:line 15 Result: 0
直接打印 e 会带出堆栈信息(以 at 开头,列出出事时一层层调用的方法和行号),只想要一句说明就打印 e.。把 0 改成 5 再跑:
| 30 | d.division(25, 5); | // 25 / 5 = 5,没出事,catch 被跳过 |
Result: 5
finally 照样执行。不管有没有异常它都会执行,关闭文件这类收尾工作就放在这里。
5自定义异常
| 21 | public class TempIsZeroException: ApplicationException | // 自定义异常类,派生自 ApplicationException |
| 22 | { | // TempIsZeroException 类开始 |
| 23 | public TempIsZeroException(string message): base(message) | // 构造函数:用 base 把消息交给父类,存进 Message |
| 24 | { | // 构造函数开始 |
| 25 | } | // 构造函数结束 |
| 26 | } | // TempIsZeroException 类结束 |
| 32 | if(temperature == 0) | // 温度是 0 |
| 33 | { | // if 开始 |
| 34 | throw (new TempIsZeroException("Zero Temperature found")); | // throw:新建一个这种异常抛出去 |
| 35 | } | // if 结束 |
查看完整代码(41 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace UserDefinedException | // 定义命名空间 |
| 3 | { | // 命名空间开始 |
| 4 | class TestTemperature | // 定义 TestTemperature 类 |
| 5 | { | // TestTemperature 类开始 |
| 6 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 7 | { | // Main 开始 |
| 8 | Temperature temp = new Temperature(); | // 创建 Temperature 对象,温度是 0 |
| 9 | try | // 可能出事的调用放进 try |
| 10 | { | // try 开始 |
| 11 | temp.showTemp(); | // 调用 showTemp:会抛出异常 |
| 12 | } | // try 结束 |
| 13 | catch(TempIsZeroException e) | // 正好接这一种自定义异常 |
| 14 | { | // catch 开始 |
| 15 | Console.WriteLine("TempIsZeroException: {0}", e.Message); | // 打印异常名和消息 |
| 16 | } | // catch 结束 |
| 17 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 18 | } | // Main 结束 |
| 19 | } | // TestTemperature 类结束 |
| 20 | } | // 命名空间结束 |
| 21 | public class TempIsZeroException: ApplicationException | // 自定义异常类 |
| 22 | { | // TempIsZeroException 类开始 |
| 23 | public TempIsZeroException(string message): base(message) | // 构造函数:消息交给父类 |
| 24 | { | // 构造函数开始 |
| 25 | } | // 构造函数结束 |
| 26 | } | // TempIsZeroException 类结束 |
| 27 | public class Temperature | // 定义 Temperature 类 |
| 28 | { | // Temperature 类开始 |
| 29 | int temperature = 0; | // 温度字段,值是 0 |
| 30 | public void showTemp() | // showTemp:显示温度 |
| 31 | { | // showTemp 开始 |
| 32 | if(temperature == 0) | // 温度是 0 |
| 33 | { | // if 开始 |
| 34 | throw (new TempIsZeroException("Zero Temperature found")); | // 抛出自定义异常 |
| 35 | } | // if 结束 |
| 36 | else | // 否则 |
| 37 | { | // else 开始 |
| 38 | Console.WriteLine("Temperature: {0}", temperature); | // 打印温度 |
| 39 | } | // else 结束 |
| 40 | } | // showTemp 结束 |
| 41 | } | // Temperature 类结束 |
TempIsZeroException: Zero Temperature found
微软现在建议自定义异常直接派生自 Exception(教程里派生自 ApplicationException 的说法已过时),改成这样再跑,输出一模一样:
| 21 | public class TempIsZeroException: Exception | // 直接派生自 Exception |
6抛出对象
在 catch 里也可以把接住的异常再抛出去,交给外层处理。注意 C# 关键字全是小写,要写 catch、throw,throw e 后面要加分号。
更要紧的是,重新抛出时写 throw; 和写 throw e; 效果不一样。下面的例子里,Inner 抛出异常,Middle 接住、记一笔、再抛出去,Main 最后接住,检查异常信息里还能不能看到最初出事的 Inner:
| 14 | catch (Exception e) | // Middle 接住 Inner 抛出的异常 |
| 15 | { | // catch 开始 |
| 16 | Console.WriteLine("Middle 先记一笔:" + e.Message); | // 先记一笔 |
| 17 | throw; | // 原样抛出:保留最初出事的位置 |
| 18 | } | // catch 结束 |
Middle 先记一笔:出错了 还能看到 Inner:True
| 17 | throw e; | // 改成 throw e;:出事位置被改成 Middle |
Middle 先记一笔:出错了 还能看到 Inner:False
查看完整代码(31 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | class Program | // 定义 Program 类 |
| 3 | { | // Program 类开始 |
| 4 | static void Inner() | // Inner:最初出事的地方 |
| 5 | { | // Inner 开始 |
| 6 | throw new InvalidOperationException("出错了"); | // 抛出一个异常 |
| 7 | } | // Inner 结束 |
| 8 | static void Middle() | // Middle:中间转手 |
| 9 | { | // Middle 开始 |
| 10 | try | // 调用 Inner 放进 try |
| 11 | { | // try 开始 |
| 12 | Inner(); | // 调用 Inner,异常从这里冒出来 |
| 13 | } | // try 结束 |
| 14 | catch (Exception e) | // 接住 |
| 15 | { | // catch 开始 |
| 16 | Console.WriteLine("Middle 先记一笔:" + e.Message); | // 打印消息 |
| 17 | throw; | // 原样抛给外层 |
| 18 | } | // catch 结束 |
| 19 | } | // Middle 结束 |
| 20 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 21 | { | // Main 开始 |
| 22 | try | // 调用 Middle 放进 try |
| 23 | { | // try 开始 |
| 24 | Middle(); | // 调用 Middle |
| 25 | } | // try 结束 |
| 26 | catch (Exception e) | // 最外层接住 |
| 27 | { | // catch 开始 |
| 28 | Console.WriteLine("还能看到 Inner:" + e.ToString().Contains("Inner")); | // 异常信息里还有没有 Inner |
| 29 | } | // catch 结束 |
| 30 | } | // Main 结束 |
| 31 | } | // Program 类结束 |
写 throw e; 线索断了。所以在 catch 里原样转手,就写 throw;。
7小结
- try 包住可能出事的语句,catch 从上往下比,最多接住一个;finally 不管出没出事都会执行。
- 每种异常都是一个类,最后都派生自
Exception。 - 自己定义异常就派生自
Exception,用throw抛出;原样转手写throw;。
下一节:finally 最常见的用处是保证文件用完一定关上,那文件本身怎么打开、怎么读写呢?
