C#专题-C# 异常处理

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 的区别
  1. throw ex会丢失真正发生异常的位置,因为会重置栈。
  2. 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 复制代码
新的异常
原始异常

异常处理最佳实践

使用异常的注意事项

  1. 异常处理应该尽量精确,避免使用过于宽泛的异常类型。
  2. 异常处理应该尽量简短,避免在catch块中执行复杂的逻辑。
  3. 异常处理应该避免使用try-catch嵌套,尽量使用单一try-catch结构。
  4. 异常处理应该避免在finally块中抛出异常,因为这会导致异常处理逻辑混乱。
  5. 异常处理应该避免在finally块中执行耗时操作,因为这会导致程序性能下降。
  6. 异常处理应该避免在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 全局异常处理)统一记录日志和处理未捕获异常,在底层仅在能够真正恢复或补充上下文时才捕获异常。

相关推荐
geovindu1 小时前
CSharp: Composite Pattern
开发语言·后端·c#·组合模式·结构型模式
影寂ldy1 小时前
C# 多线程进阶知识点(线程优先级、多委托传参、线程锁、死锁)
开发语言·数据库·c#
wrq14718 小时前
MyAccess 完整使用手册
c#·orm框架
z落落21 小时前
C# WinForm 自定义控件
开发语言·c#
lzhdim1 天前
20260714 - 个人小作品更新
c#·notenet
小堂子这厢有礼了1 天前
Chet.QuartzNet.UI v2.3.0 大更新!表格重构 + 系统配置 + 分析页全面升级!
前端·后端·ui·重构·c#·vue
asdzx672 天前
使用 Python 精准操控 Word 字体:获取与替换方案
python·c#·word
逝水无殇2 天前
C# 特性详解
开发语言·后端·c#
LongtengGensSupreme2 天前
C#图像内存高速拷贝:使用Marshal.AllocHGlobal与ArrayPool实现内存拷贝的几个方法
开发语言·数码相机·c#