C# 异常处理
C# 的异常处理(Exception Handling)是 .NET 中处理运行时错误的标准机制,它能够将正常业务逻辑与错误处理逻辑分离,提高程序的健壮性和可维护性。
异常对象
异常对象包含大量信息。
csharp
catch (Exception ex)
{
//Console.WriteLine($"{ex}");
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
Console.WriteLine(ex.Source);
Console.WriteLine(ex.InnerException);
Console.WriteLine(ex.HelpLink);
Console.WriteLine(ex.TargetSite);
}
主要的属性有:
| 属性 | 说明 |
|---|---|
| Message | 异常描述 |
| StackTrace | 调用栈 |
| Source | 程序集 |
| InnerException | 内部异常 |
| HelpLink | 帮助链接 |
| TargetSite | 发生异常的方法 |
C#异常体系
C#的异常类都继承与System.Exception类,Exception类又继承自System.Object类。因此,所有的异常类都是System.Object类的子类。
plaintext
System.Object
└── System.Exception
常见的异常如下:
plaintext
Exception
├── SystemException
│ ├── ArgumentException
│ │ ├── ArgumentNullException
│ │ └── ArgumentOutOfRangeException
│ │
│ ├── InvalidOperationException
│ ├── NullReferenceException
│ ├── DivideByZeroException
│ ├── IndexOutOfRangeException
│ ├── FormatException
│ ├── OverflowException
│ ├── IOException
│ └── TimeoutException
│
└── ApplicationException(基本不用)
实际开发过程中常见的异常:
| 异常 | 说明 |
|---|---|
| NullReferenceException | 空引用 |
| ArgumentNullException | 参数为空 |
| ArgumentException | 参数错误 |
| InvalidOperationException | 对象当前状态不能执行 |
| IOException | 文件IO异常 |
| UnauthorizedAccessException | 权限不足 |
| TaskCanceledException | Task取消 |
| OperationCanceledException | 取消操作 |
异常处理
try-catch-finally
csharp
try
{
// 可能抛出异常的代码
}
catch (ExceptionType ex)
{
// 处理异常
}
finally
{
// 无论是否发生异常都会执行的代码
}
try块包含可能会抛出异常的代码。catch块用于捕获并处理特定的异常类型。可以有多个catch块,每个块处理一种或多种异常类型。finally块包含无论是否发生异常都会执行的代码,通常用于释放资源。
多catch
多catch应该从
csharp
try
{
// 可能抛出异常的代码
}
catch (ArgumentNullException ex)
{
// 处理ArgumentNullException异常
}
catch (ArgumentException ex)
{
// 处理ArgumentException异常
}
catch (Exception ex)
{
// 处理其他异常
}
finally
{
// 无论是否发生异常都会执行的代码
}
finally
finally 无论是否异常都会执行。
例如:
csharp
FileStream fs = null;
try
{
fs = File.OpenRead("test.txt");
}
finally
{
fs?.Dispose();
}
即使发生异常:
plaintext
OpenRead
↓
异常
↓
finally
仍然会释放资源。
csharp
try {
SetAgeByInput(-1);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
finally {
Console.WriteLine("正常执行finally逻辑");
}
输出:
plaintext
年龄不能小于0
at UserQuery.<Main>g__SetAgeByInput|4_1(Int32 age) in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\osatub\LINQPadQuery:line 16
at UserQuery.Main() in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\osatub\LINQPadQuery:line 22
正常执行finally逻辑
throw
主动抛出异常 例如:
csharp
if (age < 0)
{
throw new ArgumentException("年龄不能小于0");
}
调用:
csharp
try
{
SetAge(-1);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
输出:
plaintext
年龄不能小于0
throw 和 throw ex 的区别
- throw ex会丢失真正发生异常的位置,因为会重置栈。
- throw会保留完整的栈。
例子如下: 使用throw抛出异常
csharp
void SetAge(int age)
{
if (age < 0) {
throw new ArgumentException("年龄不能小于0");
}
}
void SetAgeByInput(int age)
{
try {
SetAge(-1);
}
catch (Exception ex) {
throw;
}
}
try {
SetAgeByInput(-1);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
输出:
plaintext
年龄不能小于0
at UserQuery.<Main>g__SetAge|4_0(Int32 age) in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\urbsfk\LINQPadQuery:line 5
at UserQuery.<Main>g__SetAgeByInput|4_1(Int32 age) in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\urbsfk\LINQPadQuery:line 13
at UserQuery.Main() in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\urbsfk\LINQPadQuery:line 22
可以看出,使用throw抛出异常时,上层捕获的时候能拿到清晰的调用栈。
使用throw ex抛出异常
csharp
void SetAgeByInput(int age)
{
try {
SetAge(-1);
}
catch (Exception ex) {
throw ex;
}
}
输出:
plaintext
年龄不能小于0
at UserQuery.<Main>g__SetAgeByInput|4_1(Int32 age) in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\qoqtci\LINQPadQuery:line 16
at UserQuery.Main() in C:\Users\20260201\AppData\Local\Temp\LINQPad8\_hanlhizy\qoqtci\LINQPadQuery:line 22
在抛出异常的时候,采用throw来代替throw ex,这样上层捕获的时候能拿到清晰的调用栈。
自定义异常
自定义异常类需要继承自System.Exception类,并可以添加自定义属性和方法。
csharp
public class MyException : Exception
{
public MyException(string message) : base(message)
{
}
}
使用:
csharp
try
{
throw new MyException("自定义异常");
}
catch (MyException ex)
{
Console.WriteLine(ex.Message);
}
实际项目中建议只有在需要表达明确业务语义或携带特定上下文时才创建自定义异常类型。
异步异常
异步方法中抛出的异常需要通过await关键字来捕获和处理。
csharp
async Task DoSomethingAsync()
{
try
{
await Task.Run(() => { throw new Exception("异步异常"); });
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
异步方法如果可能抛出异常,不能直接执行,需要通过
await关键字来捕获和处理。 原因是 1.Task没人等待 2.异常可能丢失 3.不容易定位
异常链
异常链是指在捕获一个异常后,再抛出一个新的异常,新的异常包含原始异常的信息。
csharp
try
{
try
{
throw new Exception("原始异常");
}
catch (Exception ex)
{
throw new Exception("新的异常", ex);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.InnerException?.Message);
}
输出:
plaintext
新的异常
原始异常
异常处理最佳实践
使用异常的注意事项
- 异常处理应该尽量精确,避免使用过于宽泛的异常类型。
- 异常处理应该尽量简短,避免在catch块中执行复杂的逻辑。
- 异常处理应该避免使用try-catch嵌套,尽量使用单一try-catch结构。
- 异常处理应该避免在finally块中抛出异常,因为这会导致异常处理逻辑混乱。
- 异常处理应该避免在finally块中执行耗时操作,因为这会导致程序性能下降。
- 异常处理应该避免在finally块中释放资源,因为这会导致资源泄露。
异常过滤
在catch异常中,可以直接在后面增加针对特定情况的处理
csharp
catch(IOException ex) when (ex.Message.Contains("locked"))
{
Console.WriteLine("文件被占用");
}
这样避免在代码中出现大量的if判断。
csharp
catch(IOException ex)
{
if(...)
{
}
}
最佳实践
不要仅捕获异常不处理
错误:
csharp
try
{
}
catch
{
}
不要用异常代替正常流程
错误
csharp
try
{
var n = int.Parse(str);
}
catch
{
}
推荐:
csharp
if (int.TryParse(str, out int n))
{
}
参数检查要尽早
chsarp
public void Save(User user)
{
ArgumentNullException.ThrowIfNull(user);
// ...
}
先记录日志后抛出异常
csharp
try
{
// ...
}
catch (Exception ex)
{
Log.Error(ex, "保存用户失败");
throw;
}
不要直接throw
不要过度捕获
例如:
csharp
public void A()
{
B();
}
public void B()
{
C();
}
public void C()
{
}
如果只有 A 能处理异常,那么就在 A 捕获:
csharp
try
{
B();
}
catch(Exception ex)
{
// 统一处理
}
而不是在每一层都写 try-catch。
不要吞掉异常
错误:
csharp
try
{
// ...
}
catch
{
// ...
}
不要在finally中抛出异常
错误:
csharp
try
{
// ...
}
catch
{
// ...
}
finally
{
throw new Exception("finally中抛出异常");
}
架构分层推荐
在典型的三层架构中:
plaintext
UI
│
▼
Application
│
▼
Domain
│
▼
Repository(Database)
建议遵循以下原则:
Repository 层:让数据库、文件、网络等底层异常自然抛出,或包装为更有意义的基础设施异常。 Domain 层:抛出业务相关异常(如库存不足、状态非法等),避免吞掉异常。 Application 层:根据业务场景决定是否转换异常、记录日志或补充上下文。 UI/API 层:统一捕获未处理异常,记录日志,并转换为友好的错误响应(如 HTTP 500、业务错误码等)。
这种方式可以避免每一层都编写重复的 try-catch,同时保证异常信息和调用栈得到保留。
总结
对于现代 C#(.NET 6/7/8/9)项目,可以遵循以下原则:
优先使用具体异常类型,避免直接捕获 Exception,除非用于统一处理。 使用 throw; 重新抛出异常,避免 throw ex; 导致调用栈丢失。 使用 using / await using 管理资源,而不是手写 finally。 对可预期的失败(如字符串转换)优先使用 TryParse 等模式,而不是依赖异常控制流程。 在应用入口(如 ASP.NET Core 中间件、WPF 全局异常处理)统一记录日志和处理未捕获异常,在底层仅在能够真正恢复或补充上下文时才捕获异常。