06-异常捕获
异常是指程序在运行时发生的错误或意外情况。C# 提供了完善的异常处理机制,使得程序在遇到错误时可以优雅地处理,而不是直接崩溃。
一、编译错误 vs 运行错误
| 对比项 | 编译错误 | 运行错误(异常) |
|---|---|---|
| 发生时间 | 编译阶段 | 程序运行时 |
| 原因 | 语法错误、类型不匹配等 | 逻辑错误、环境问题等 |
| 影响 | 无法生成可执行文件 | 程序可能崩溃 |
| 处理方式 | 编译器提示,修正代码 | try-catch 捕获或修正代码 |
| 示例 | 缺少分号、变量未声明 | 除零、空引用、数组越界 |
csharp
// 编译错误——编译时发现,无法运行
// int number = "Hello"; // 类型不匹配
// 运行错误——编译通过,运行时崩溃
int divisor = 0;
// int result = 10 / divisor; // DivideByZeroException二、.NET 常用异常类型
| 异常类型 | 说明 | 触发场景 |
|---|---|---|
System.Exception | 所有异常的基类 | 通用的异常捕获 |
System.SystemException | 系统抛出的异常基类 | — |
System.NullReferenceException | 空引用异常 | 调用了 null 对象的成员 |
System.IndexOutOfRangeException | 索引越界 | 数组/集合索引超出范围 |
System.DivideByZeroException | 除零异常 | 整数除以 0 |
System.ArgumentException | 参数异常 | 方法参数不合法 |
System.ArgumentNullException | 参数为空异常 | null 参数不允许时 |
System.ArgumentOutOfRangeException | 参数超出范围 | 索引/值超出允许范围 |
System.FormatException | 格式异常 | 字符串格式不匹配 |
System.InvalidCastException | 无效转换异常 | 类型转换失败 |
System.InvalidOperationException | 无效操作异常 | 对象状态不允许操作 |
System.OverflowException | 溢出异常 | 算术运算溢出 |
System.IO.IOException | IO 异常 | 文件读写错误 |
System.IO.FileNotFoundException | 文件未找到 | 文件不存在 |
System.UnauthorizedAccessException | 访问权限异常 | 没有权限访问资源 |
三、异常捕获机制
1. 基本结构:try-catch-finally
csharp
try
{
// 可能抛出异常的代码
int result = 10 / 0;
}
catch (Exception ex)
{
// 处理异常
Console.WriteLine($"发生异常:{ex.Message}");
Console.WriteLine($"异常类型:{ex.GetType().Name}");
Console.WriteLine($"堆栈跟踪:{ex.StackTrace}");
}
finally
{
// 无论是否异常都会执行(可选)
Console.WriteLine("清理资源");
}2. 捕获特定异常
csharp
try
{
string? str = null;
Console.WriteLine(str.Length); // NullReferenceException
}
catch (NullReferenceException ex)
{
Console.WriteLine($"空引用:{ex.Message}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"除零:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"其他异常:{ex.Message}");
}注意: 异常捕获按顺序匹配,应将更具体的异常写在前面,通用的
Exception写在最后。
3. 异常过滤器(when 关键字,C# 6.0+)
csharp
try
{
// 可能抛出异常的代码
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("404 错误");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError)
{
Console.WriteLine("500 错误");
}
catch (Exception ex) when (Log(ex)) // 只记录不捕获
{
// 这里不会执行,因为 Log 返回 false
}
static bool Log(Exception ex)
{
Console.WriteLine($"记录异常:{ex.Message}");
return false; // 返回 false 表示不捕获
}4. finally 块
csharp
// finally 中的代码总会执行——无论是否异常,无论是否有 return
static string TestFinally()
{
try
{
Console.WriteLine("try 执行");
return "返回值";
}
catch (Exception ex)
{
Console.WriteLine($"catch 执行:{ex.Message}");
return "异常返回值";
}
finally
{
Console.WriteLine("finally 执行——即使在 return 之后");
}
}
// finally 常用于释放资源
FileStream? file = null;
try
{
file = File.OpenRead(@"C:\test.txt");
// 处理文件...
}
catch (FileNotFoundException)
{
Console.WriteLine("文件未找到");
}
finally
{
file?.Close(); // 确保文件被关闭
}5. using 语句——try-finally 的语法糖
csharp
// 以下两种写法等价:
// 写法一:传统 try-finally
FileStream fs1 = new FileStream(@"C:\test.txt", FileMode.Open);
try
{
// 使用文件...
}
finally
{
fs1.Dispose();
}
// 写法二:using 语句(推荐)
using (FileStream fs2 = new FileStream(@"C:\test.txt", FileMode.Open))
{
// 使用文件...
} // 自动调用 Dispose()
// 写法三:using 声明(C# 8.0+)
using FileStream fs3 = new FileStream(@"C:\test.txt", FileMode.Open);
// 使用文件...
// 作用域结束时自动释放四、抛出异常
1. throw 语句
csharp
static void ValidateAge(int age)
{
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age), "年龄不能为负数");
if (age > 150)
throw new ArgumentException("年龄超出合理范围", nameof(age));
}
// 使用
try
{
ValidateAge(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine($"参数错误:{ex.Message}");
}2. 异常再抛出
csharp
try
{
// 某些操作
}
catch (Exception ex)
{
// 记录日志后重新抛出
// ✅ 正确:保留原始堆栈信息
throw;
// ❌ 错误:重置堆栈信息,丢失原始调用链
// throw ex;
// ✅ 包装为新的异常(保留原始异常作为 InnerException)
// throw new ApplicationException("发生错误", ex);
}五、自定义异常
csharp
// 自定义异常类需要继承 Exception
public class OrderException : Exception
{
// 默认构造函数
public OrderException() : base("订单处理错误") { }
// 带消息
public OrderException(string message) : base(message) { }
// 带消息和内部异常
public OrderException(string message, Exception inner) : base(message, inner) { }
// 自定义属性
public string OrderId { get; set; }
}
// 使用自定义异常
void ProcessOrder(string orderId)
{
try
{
// 处理订单...
throw new InvalidOperationException("库存不足");
}
catch (Exception ex)
{
throw new OrderException($"订单 {orderId} 处理失败")
{
OrderId = orderId
};
}
}
// 捕获自定义异常
try
{
ProcessOrder("ORD-001");
}
catch (OrderException ex)
{
Console.WriteLine($"订单错误:{ex.Message},订单号:{ex.OrderId}");
}六、InnerException
当在 catch 块中抛出新异常时,可将原始异常作为内部异常传递。
csharp
try
{
try
{
int.Parse("invalid"); // 原始异常:FormatException
}
catch (FormatException ex)
{
// 包装为新异常,保留原始异常信息
throw new InvalidOperationException("处理数据时出错", ex);
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"消息:{ex.Message}");
Console.WriteLine($"内部异常:{ex.InnerException?.GetType().Name}");
Console.WriteLine($"内部异常消息:{ex.InnerException?.Message}");
}七、异常处理最佳实践
1. 在合适的层级捕获异常
csharp
// ❌ 不要在每个方法中都 catch——太冗长
void Method1()
{
try { /* ... */ }
catch (Exception ex) { Log(ex); throw; }
}
// ✅ 在顶层统一处理
void TopLevel()
{
try
{
Method1();
}
catch (Exception ex)
{
Log(ex);
ShowError(ex.Message);
}
}2. 捕获特定异常而非 Exception
csharp
// ❌ 不推荐:捕获所有异常
try { /* ... */ }
catch (Exception ex) { /* 处理 */ }
// ✅ 推荐:只捕获你知道如何处理的异常
try { /* ... */ }
catch (FileNotFoundException ex) { /* 文件不存在 */ }
catch (UnauthorizedAccessException ex) { /* 权限不足 */ }3. 避免静默吞异常
csharp
// ❌ 危险:吞掉异常,程序可能处于不一致状态
try { /* ... */ }
catch (Exception) { /* 什么都不做 */ }
// ✅ 至少记录日志
try { /* ... */ }
catch (Exception ex)
{
Logger.Log(ex);
// 然后根据情况决定:重新抛出 / 返回默认值 / 提示用户
}4. 使用 finally 释放资源
csharp
// 使用 using 或 finally 确保资源释放
using var connection = new SqlConnection(connectionString);
// 使用连接...
// 自动释放5. 不要让异常影响控制流
csharp
// ❌ 不推荐:用异常控制流程
try
{
int.Parse(userInput);
}
catch (FormatException)
{
Console.WriteLine("无效输入");
}
// ✅ 推荐:用 TryParse 避免异常
if (int.TryParse(userInput, out int result))
{
Console.WriteLine($"有效数字:{result}");
}
else
{
Console.WriteLine("无效输入");
}八、综合案例
csharp
// 用户注册案例——展示异常处理的完整使用
public class UserRegistration
{
public class RegistrationException : Exception
{
public RegistrationException(string message, Exception? inner = null)
: base(message, inner) { }
}
public void Register(string username, string email, int age)
{
// 参数验证
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentException("用户名不能为空", nameof(username));
if (!email.Contains("@"))
throw new ArgumentException("邮箱格式无效", nameof(email));
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "年龄不合法");
try
{
// 模拟数据库操作
SaveToDatabase(username, email, age);
Console.WriteLine("注册成功");
}
catch (SqlException ex) when (ex.Number == 2627) // 主键冲突
{
throw new RegistrationException("用户名已存在", ex);
}
catch (IOException ex)
{
throw new RegistrationException("数据库连接失败,请稍后重试", ex);
}
finally
{
Console.WriteLine("注册操作结束");
}
}
private void SaveToDatabase(string username, string email, int age)
{
// 模拟可能的异常
throw new IOException("数据库连接超时");
}
}
// 使用
try
{
var registration = new UserRegistration();
registration.Register("张三", "invalid-email", 25);
}
catch (ArgumentException ex)
{
Console.WriteLine($"输入错误:{ex.Message}");
}
catch (UserRegistration.RegistrationException ex)
{
Console.WriteLine($"注册失败:{ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($"原因:{ex.InnerException.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"意外错误:{ex.Message}");
}核心知识点总结
异常处理结构
| 结构 | 说明 | 是否必需 |
|---|---|---|
try | 包裹可能抛出异常的代码 | 必需 |
catch | 捕获并处理特定异常 | 可选(至少一个 catch 或 finally) |
finally | 无论是否异常都执行 | 可选 |
异常处理原则
| 原则 | 说明 |
|---|---|
| 精确捕获 | 捕获你能处理的特定异常类型 |
| 及时清理 | 使用 using 或 finally 释放资源 |
| 避免吞异常 | 不要写空的 catch 块 |
| 保留堆栈 | 使用 throw;(不是 throw ex;)重新抛出 |
| 不要用异常控制流程 | 使用 TryParse 等模式替代 |
| 自定义异常 | 继承 Exception,命名以 Exception 结尾 |
| 调用链信息 | 通过 InnerException 保留原始异常 |
注意事项
- 异常开销大——不要用异常控制正常的程序流程
- catch 顺序——具体异常在前,通用异常在后
- finally 总会执行——即使 try 中有 return
- throw vs throw ex——
throw保留原始堆栈,throw ex重置堆栈 - 异常过滤器性能更好——
when子句匹配失败时不会 unwind 堆栈 - 不要捕获致命异常——
OutOfMemoryException、StackOverflowException等无法安全处理 - 记录足够信息——异常消息、类型、堆栈、自定义数据等


