C#异常(十七)
异常概述
异常定义:异常属于运行时错误,是违背系统/程序约束的非正常状况,发生时系统会捕获错误并抛出(raise)异常对象。例如,程序试图将一个数除以0或试图写入一个只读文件。
未处理异常后果:代码如果没有异常处理逻辑,程序直接崩溃终止,输出原始、不友好的报错信息,示例中除零会抛出DivideByZeroException。
1 | static void Main() |
1 | Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. |
异常处理的目的:
- 条件允许时修复问题,让程序继续运行;
- 留存异常日志,方便开发排查;
- 释放数据库连接这类外部资源;
- 给用户展示易懂的提示。
原则:如果无法修复异常,处理完资源与日志后应当终止程序,不要在异常未知的不稳定状态继续运行。
try语句
- C# 的
try语句用于异常防护与异常处理,分为三大部分:try块、catch子句、finally块。 - try 块:包裹需要被保护、有可能抛出异常的业务代码。
- catch 子句:可以写一个或者多个,是捕获、处理对应异常的逻辑代码(异常处理程序)。
- finally 块:无论程序是否抛出异常、是否被
catch捕获,该块内部代码一定会执行,一般用于资源释放清理。
语法组合说明:
try后面至少搭配catch或者finally其中一种;可以try‑catch、try‑finally、try‑catch‑finally。
![[csharp-17-catch-try.png]]
处理异常示例:
- 将可能出错(除零)的代码放入
try块,配合无参数catch可以捕获发生的异常。 - 异常触发后,程序跳转到
catch块内部执行异常处理逻辑,不会程序崩溃终止,处理完成之后程序继续向下运行。 - 捕获成功之后控制台不会输出系统原生的报错堆栈,只会输出
catch块内自定义打印的提示文本。 - 该示例为通用捕获,能够捕获
try块抛出的任意异常类型。
1 | static void Main() |
1 | Handling all exceptions - Keep on Running |
异常类
程序中可以发生许多不同类型的异常。BCL定义了许多异常类,每一个类代表一种指定的异常类型。当一个异常发生时,CLR创建该类型的异常对象并寻找适当的catch子句以处理它。
C#中所有异常类都派生自System.Exception类,而System.Exception类派生自System.Object。
继承层次简要:
System.Exception:所有异常类的基类SystemException:所有预定义系统异常的基类IndexOutOfRangeException索引越界异常NullReferenceException空引用异常IOExceptionIO 异常
ApplicationException:所有非致命的应用程序自定义异常的基类
异常对象含有只读属性,带有导致该异常的异常信息。这一信息有助于调试应用程序,防止将来发生类似的异常。
| 属性 | 类型 | 描述 |
|---|---|---|
| Message | string | 这个属性含有解释异常原因的错误消息 |
| StackTrace | string | 这个属性含有描述异常发生在何处的信息 |
| InnerException | Exception | 如果当前异常是由另一个异常引起的,则这个属性包含前一个异常的引用 |
| Source | string | 如果没有被应用程序定义的异常设定,那么这个属性含有异常所在的程序集的名称 |
catch子句
catch 子句处理异常。它有 4 种形式,允许不同级别的处理。
| 语法格式 | 描述 |
|---|---|
catch { Statements } |
一般catch子句 1. 在catch关键字之后没有参数列表 2. 匹配try块中抛出的任何类型的异常 |
catch( ExceptionType ) { Statements } |
特定catch子句 1. 以一个异常类的名称作为单一参数 2. 匹配任何指定类型的异常 |
catch( ExceptionType ExceptionVariable ) { Statements } |
带对象的特定catch子句 1. 在异常类名称之后包括一个标识符 2. 该标识符在catch子句块中相当于一个局部变量,并被称为异常变量 3. 异常变量引用异常对象,并能用于访问关于该对象的信息 |
catch( ExceptionType ExceptionVariable ) when( predicate ) { Statements } |
带谓词的特定catch子句 1. 在异常类名称之后包括一个标识符 2. 该标识符在catch子句块中相当于一个局部变量,并被称为异常变量 3. 异常变量引用异常对象,并能用于访问关于该对象的信息 4. 只有当谓词的计算结果为true时,才能进入子句块 |
使用特点catch子句的示例:
1 | int x = 10; |
1 | int x = 10; |
1 | Message: Attempted to divide by zero. |
异常过滤器
when异常过滤器是 C#6.0 新增语法,属于catch第四种形式;可以针对同一种异常类型设置多个catch分支,依靠额外条件区分处理逻辑,避免在单个catch块写大量if判断。
匹配规则:从上到下顺序匹配,只会执行第一个满足【异常类型匹配+when条件为true】的catch子句,匹配成功后后续catch不再判断。
1 | try |
when子句规则要点:
- 内部是返回
bool的谓词表达式; - 不支持异步,不适合任何需要长时间运行的操作;
when表达式内部抛出的异常会直接被静默忽略,会增加这部分代码的调试难度。
优势:把条件判断放在 catch 筛选阶段,代码职责更清晰,异常处理分支更专一。
catch子句段
带参数的catch子句会拿到异常对象引用,用来查看异常信息;InnerException 属性用于获取引发当前异常的原始内层异常,用于追溯异常根源。
异常匹配规则:从上往下依次查找,执行第一个类型匹配成功的catch,因此catch顺序至关重要。
排序强制规则:
- 先写更具体、派生程度高的异常类型,后写基类、更通用的异常;
- 无参数的通用
catch必须写在全部特定 catch 的末尾。
尽量不要滥用通用catch,它会掩盖错误,优先捕获明确的特定异常类型。
finally块
核心规则:只要控制流进入了try,finally块一定会执行。
两种正常流程:
try无异常:try执行完毕-→跳过所有catch-→执行finally;try抛出异常:匹配并执行对应catch-→执行finally。
特殊场景:哪怕try内部写return提前返回,也不会跳过finally;会先执行finally,再返回调用方。
使用场景:finally一般用来做资源清理(关闭文件、释放连接等),保证资源一定释放。
补充:极少数极端情况
finally不会执行,例如Environment.Exit()终止进程、进程被操作系统强制杀死。
1 | try |
1 | First Branch - In finally statement |
为异常寻找处理程序
当try内抛出异常,编译器会在当前try的多个catch里,从上往下查找能匹配该异常类型的catch子句。
找到匹配catch后的执行顺序:执行catch块-→执行finally块(存在时)-→跳出整个try结构,继续执行try语句之后的代码。
两种结构对比:
- 无
finally:catch执行完成,直接跳到try-catch结构后面继续运行; - 有
finally:catch执行完成,必须先执行finally,再向后继续执行。
要点:catch捕获处理异常之后,异常相当于被消化,不会继续向外层抛出。
进一步搜索
- 调用栈搜索机制:当前方法找不到匹配的
catch,异常会沿着调用栈向上回溯,依次去外层调用方法寻找能够处理该异常的catch。 - 执行顺序:找到外层匹配
catch前,先执行当前方法的finally,然后弹出当前方法,再执行外层catch与外层finally。 - 搜索顺序:先本地方法-→再外层调用方法,一层一层向上查找。
- 如果整条调用栈全部查找完毕,依旧没有找到匹配
catch,程序终止。
哪怕异常向上抛出,途经的每一层带
finally的代码,finally都会执行。
一般法则
异常处理的一般法则:
![[csharp-17-catch-search.png]]
搜索调用栈的示例
1 | class Program |
1 | finally clause in B() |
抛出异常
使用 throw 语句使代码显式地抛出异常。throw语句的语法:throw ExceptionObject;
1 | class MyClass |
1 | Message: Value cannot be null. |
不带异常对象的抛出
throw语句还可以在catch块内部不带异常对象使用。
- 这种形式重新抛出当前异常,系统继续搜索,为该异常寻找另外的处理程序。
- 这种形式(无参数)只能用在
catch语句内部。
1 | class MyClass |
1 | Inner Catch: Value cannot be null. |
throw表达式
throw表达式(C#7.0新增):在只允许写表达式的位置使用throw,而传统throw是语句,C#7.0前不能放在??、三元运算符?:内部。
适用场景:
- 空合并运算符
??:变量为null时直接抛出异常 - 三元条件运算符
?::分支中抛出异常
语法:throw new 异常类(),写法和throw语句一样,编译器自动区分是语句还是表达式。
优势:简化属性、lambda、表达式体成员的参数校验,代码更精简。
1 | // 示例2:三元条件运算符搭配 throw 表达式 |
1 | Not safe to get code. |