异常概述

异常定义:异常属于运行时错误,是违背系统/程序约束的非正常状况,发生时系统会捕获错误并抛出(raise)异常对象。例如,程序试图将一个数除以0或试图写入一个只读文件。
未处理异常后果:代码如果没有异常处理逻辑,程序直接崩溃终止,输出原始、不友好的报错信息,示例中除零会抛出DivideByZeroException。

1
2
3
4
5
static void Main()
{
int x = 10, y = 0;
x /= y; //用0除一个数时抛出一个异常
}
1
2
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
at Exceptions_1.Program.Main() in C:\Progs\Exceptions\Program.cs:line 12.

异常处理的目的:

  • 条件允许时修复问题,让程序继续运行;
  • 留存异常日志,方便开发排查;
  • 释放数据库连接这类外部资源;
  • 给用户展示易懂的提示。

原则:如果无法修复异常,处理完资源与日志后应当终止程序,不要在异常未知的不稳定状态继续运行。

try语句

  1. C# 的try语句用于异常防护与异常处理,分为三大部分:try块、catch子句、finally块。
  2. try 块:包裹需要被保护、有可能抛出异常的业务代码。
  3. catch 子句:可以写一个或者多个,是捕获、处理对应异常的逻辑代码(异常处理程序)。
  4. finally 块:无论程序是否抛出异常、是否被catch捕获,该块内部代码一定会执行,一般用于资源释放清理。

语法组合说明:try后面至少搭配catch或者finally其中一种;可以try‑catch、try‑finally、try‑catch‑finally。

![[csharp-17-catch-try.png]]

处理异常示例:

  1. 将可能出错(除零)的代码放入try块,配合无参数catch可以捕获发生的异常。
  2. 异常触发后,程序跳转到catch块内部执行异常处理逻辑,不会程序崩溃终止,处理完成之后程序继续向下运行。
  3. 捕获成功之后控制台不会输出系统原生的报错堆栈,只会输出catch块内自定义打印的提示文本。
  4. 该示例为通用捕获,能够捕获try块抛出的任意异常类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Main()
{
int x = 10;
try
{
int y = 0;
x /= y; //抛出一个异常
}
catch
{
... //处理异常的代码
Console.WriteLine("Handling all exceptions - Keep on Running");
}
}
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
2
3
4
5
6
7
8
9
10
int x = 10;
try
{
int y = 0;
x /= y; //抛出一个异常
}
catch (DivideByZeroException )
{
Console.WriteLine("Handling an exception.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
int x = 10;
try
{
int y = 0;
x /= y; //抛出一个异常
}
catch ( DivideByZeroException e ) //异常类型 + 异常变量e
{
//访问异常对象的属性
Console.WriteLine("Message: {0}", e.Message );
Console.WriteLine("Source: {0}", e.Source );
Console.WriteLine("Stack: {0}", e.StackTrace );
}
1
2
3
Message: Attempted to divide by zero.
Source: Exceptions 1
Stack: at Exceptions_1.Program.Main() in C:\Progs\Exceptions 1\Exceptions 1\Program.cs:line 14

异常过滤器

when异常过滤器是 C#6.0 新增语法,属于catch第四种形式;可以针对同一种异常类型设置多个catch分支,依靠额外条件区分处理逻辑,避免在单个catch块写大量if判断。
匹配规则:从上到下顺序匹配,只会执行第一个满足【异常类型匹配+when条件为true】的catch子句,匹配成功后后续catch不再判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try
{
// ... 执行某个Web请求
}
// 过滤器:异常消息包含"307"才进入此catch
catch ( HttpRequestException e ) when ( e.Message.Contains("307") )
{
// ... 采取某种行动
}
// 过滤器:异常消息包含"301"才进入此catch
catch ( HttpRequestException e ) when ( e.Message.Contains("301") )
{
// ... 采取其他行动
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
{
if (inVal < 10)
{
Console.Write("First Branch - ");
return; // 执行return,不会跳过finally
}
else
Console.Write("Second Branch - ");
}
finally
{
// 无论try里是否return、是否抛异常,finally一定会执行
Console.WriteLine("In finally statement");
}
1
First Branch - In finally statement

为异常寻找处理程序

当try内抛出异常,编译器会在当前try的多个catch里,从上往下查找能匹配该异常类型的catch子句。
找到匹配catch后的执行顺序:执行catch块-→执行finally块(存在时)-→跳出整个try结构,继续执行try语句之后的代码。
两种结构对比:

  • 无finally:catch执行完成,直接跳到try-catch结构后面继续运行;
  • 有finally:catch执行完成,必须先执行finally,再向后继续执行。

要点:catch捕获处理异常之后,异常相当于被消化,不会继续向外层抛出。

进一步搜索

  1. 调用栈搜索机制:当前方法找不到匹配的catch,异常会沿着调用栈向上回溯,依次去外层调用方法寻找能够处理该异常的catch。
  2. 执行顺序:找到外层匹配catch前,先执行当前方法的finally,然后弹出当前方法,再执行外层catch与外层finally。
  3. 搜索顺序:先本地方法-→再外层调用方法,一层一层向上查找。
  4. 如果整条调用栈全部查找完毕,依旧没有找到匹配catch,程序终止。

哪怕异常向上抛出,途经的每一层带finally的代码,finally都会执行。

一般法则

异常处理的一般法则:
![[csharp-17-catch-search.png]]

搜索调用栈的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Program
{
static void Main()
{
MyClass MCls = new MyClass();
try
{
MCls.A(); // 调用A方法
}
catch (DivideByZeroException )
{
Console.WriteLine("catch clause in Main()");
}
finally
{
Console.WriteLine("finally clause in Main()");
}
Console.WriteLine("After try statement in Main.");
Console.WriteLine(" -- Keep running.");
}
}

class MyClass
{
public void A()
{
try
{
B(); // A内部调用B方法
}
catch (NullReferenceException)
{
Console.WriteLine("catch clause in A()");
}
finally
{
Console.WriteLine("finally clause in A()");
}
}

void B()
{
int x = 10, y = 0;
try
{
x /= y; // 除以0,抛出DivideByZeroException
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("catch clause in B()");
}
finally
{
Console.WriteLine("finally clause in B()");
}
}
}
1
2
3
4
5
6
finally clause in B()
finally clause in A()
catch clause in Main()
finally clause in Main()
After try statement in Main.
-- Keep running.

抛出异常

使用 throw 语句使代码显式地抛出异常。throw语句的语法:throw ExceptionObject;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MyClass
{
public static void PrintArg(string arg)
{
try
{
if (arg == null)
{
// 新建参数空异常实例,传入参数名arg
ArgumentNullException myEx = new ArgumentNullException("arg");
throw myEx; // 显式抛出异常
}
Console.WriteLine(arg);
}
catch (ArgumentNullException e)
{
// 捕获抛出的空参数异常,打印消息
Console.WriteLine($"Message: { e.Message }");
}
}
}

class Program
{
static void Main()
{
string s = null;
MyClass.PrintArg(s); // 传入null,触发异常
MyClass.PrintArg("Hi there!"); // 传入有效字符串,正常执行
}
}
1
2
3
Message: Value cannot be null.
Parameter name: arg
Hi there!

不带异常对象的抛出

throw语句还可以在catch块内部不带异常对象使用。

  • 这种形式重新抛出当前异常,系统继续搜索,为该异常寻找另外的处理程序。
  • 这种形式(无参数)只能用在catch语句内部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyClass
{
public static void PrintArg(string arg)
{
try // 外层try
{
try // 内层try
{
if (arg == null)
{
// 创建参数空异常,传入参数名arg
ArgumentNullException myEx = new ArgumentNullException("arg");
throw myEx; // 抛出异常
}
Console.WriteLine(arg);
}
catch (ArgumentNullException e) // 内层catch捕获
{
Console.WriteLine($"Inner Catch: {e.Message}");
throw; // 不带参数,重新抛出当前异常(保留原始堆栈信息)
}
}
catch // 外层通用catch,捕获内层重新抛出的异常
{
Console.WriteLine("Outer Catch: Handling an Exception.");
}
}
}

class Program
{
static void Main()
{
string s = null;
MyClass.PrintArg(s); // null参数触发异常
MyClass.PrintArg("Hi there!"); // 正常参数
}
}
1
2
3
4
Inner Catch: Value cannot be null.
Parameter name: arg
Outer Catch: Handling an Exception.
Hi there!

throw表达式

throw表达式(C#7.0新增):在只允许写表达式的位置使用throw,而传统throw是语句,C#7.0前不能放在??、三元运算符?:内部。
适用场景:

  • 空合并运算符 ??:变量为null时直接抛出异常
  • 三元条件运算符 ?::分支中抛出异常

语法:throw new 异常类(),写法和throw语句一样,编译器自动区分是语句还是表达式。

优势:简化属性、lambda、表达式体成员的参数校验,代码更精简。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 示例2:三元条件运算符搭配 throw 表达式
class Program
{
static string SecretCode { get { return "Roses are red"; } }
static void Main()
{
bool safe = false;
try
{
// 三元运算符,条件不满足时执行throw表达式
string secretCode = safe
? SecretCode
: throw new Exception("Not safe to get code.");
Console.WriteLine($"Code is: {secretCode}.");
}
catch (Exception e)
{
Console.WriteLine($"{ e.Message }");
}
}
}
1
Not safe to get code.