异常处理
try/catch/finally 基本机制
C# 使用 try、catch、finally 三个关键字构建异常处理机制。try 块中放置可能抛出异常的代码,catch 块捕获并处理异常,finally 块无论是否发生异常都会执行,常用于资源释放。
CSHARP
try
{
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"捕获异常:{ex.Message}");
}
finally
{
Console.WriteLine("finally 块始终执行");
}
TEXT
捕获异常:Attempted to divide by zero.
finally 块始终执行
多个 catch 块
可以按顺序捕获不同类型的异常,范围小的异常类型应放在前面。
CSHARP
try
{
int[] arr = { 1, 2, 3 };
Console.WriteLine(arr[10]);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"索引越界:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"通用异常:{ex.Message}");
}
TEXT
索引越界:Index was outside the bounds of the array.
常见异常类型
| 异常类型 | 触发场景 |
|---|---|
FormatException |
字符串格式不正确,如 int.Parse("abc") |
NullReferenceException |
访问 null 对象的成员 |
IndexOutOfRangeException |
数组索引超出范围 |
DivideByZeroException |
整数除以零 |
OverflowException |
算术溢出(checked 上下文中) |
FileNotFoundException |
文件不存在 |
ArgumentException |
方法参数无效 |
示例
CSHARP
try
{
int num = int.Parse("hello");
}
catch (FormatException)
{
Console.WriteLine("格式异常:字符串无法转换为整数");
}
string s = null;
try
{
Console.WriteLine(s.Length);
}
catch (NullReferenceException)
{
Console.WriteLine("空引用异常:对象为 null");
}
TEXT
格式异常:字符串无法转换为整数
空引用异常:对象为 null
异常类层次结构
所有异常类的继承关系如下:
TEXT
Object
└─ Exception
└─ SystemException
├─ FormatException
├─ NullReferenceException
├─ IndexOutOfRangeException
├─ DivideByZeroException
├─ OverflowException
├─ FileNotFoundException
└─ ArgumentException
└─ ArgumentNullException
Exception 类属性
Exception 类提供了以下常用属性用于获取异常的详细信息:
| 属性 | 说明 |
|---|---|
Message |
描述异常的可读文本 |
StackTrace |
异常发生时的调用堆栈信息 |
InnerException |
导致当前异常的内部异常 |
Source |
引发异常的应用程序或对象名称 |
TargetSite |
抛出异常的方法 |
示例
CSHARP
try
{
int zero = 0;
int result = 100 / zero;
}
catch (Exception ex)
{
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine($"Source: {ex.Source}");
Console.WriteLine($"TargetSite: {ex.TargetSite}");
}
TEXT
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()
throw 抛出异常
使用 throw 关键字可以主动抛出异常。重新抛出时,throw; 保留原始堆栈信息,而 throw ex; 会重置堆栈,应避免使用后者。
示例
CSHARP
void CheckAge(int age)
{
if (age < 0)
{
throw new ArgumentException("年龄不能为负数", nameof(age));
}
Console.WriteLine($"年龄:{age}");
}
try
{
CheckAge(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
TEXT
年龄不能为负数 (Parameter 'age')
throw 与 throw ex 的区别
CSHARP
void InnerMethod()
{
throw new InvalidOperationException("内部错误");
}
void OuterMethod()
{
try
{
InnerMethod();
}
catch (Exception ex)
{
throw;
}
}
try
{
OuterMethod();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
TEXT
at InnerMethod() in Program.cs:line 2
at OuterMethod() in Program.cs:line 10
💡 提示: 始终使用
throw; 重新抛出异常,它保留完整的调用堆栈,便于定位问题根源。throw ex; 会将堆栈截断到当前方法,丢失原始异常位置信息。
异常筛选器
C# 6 引入了异常筛选器 when 子句,允许在 catch 中添加条件,只有条件为 true 时才捕获该异常。
示例
CSHARP
try
{
throw new HttpRequestException("网络超时,请稍后重试");
}
catch (HttpRequestException ex) when (ex.Message.Contains("超时"))
{
Console.WriteLine("捕获超时异常,准备重试");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"其他网络异常:{ex.Message}");
}
TEXT
捕获超时异常,准备重试
自定义异常类
通过继承 Exception 创建自定义异常类,通常需要实现三个构造函数:无参构造函数、带消息参数的构造函数、带消息和内部异常的构造函数。
示例
CSHARP
class InsufficientBalanceException : Exception
{
public decimal Balance { get; }
public decimal Amount { get; }
public InsufficientBalanceException()
: base("余额不足") { }
public InsufficientBalanceException(string message)
: base(message) { }
public InsufficientBalanceException(string message, Exception innerException)
: base(message, innerException) { }
public InsufficientBalanceException(decimal balance, decimal amount)
: base($"余额不足:当前余额 {balance},需要 {amount}")
{
Balance = balance;
Amount = amount;
}
}
class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal balance)
{
Balance = balance;
}
public void Withdraw(decimal amount)
{
if (amount > Balance)
{
throw new InsufficientBalanceException(Balance, amount);
}
Balance -= amount;
Console.WriteLine($"取款成功,余额:{Balance}");
}
}
var account = new BankAccount(100);
try
{
account.Withdraw(200);
}
catch (InsufficientBalanceException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"余额:{ex.Balance},尝试取款:{ex.Amount}");
}
TEXT
余额不足:当前余额 100,需要 200
余额:100,尝试取款:200
异常处理最佳实践
捕获具体异常,不要只捕获 Exception
CSHARP
try
{
int value = int.Parse(input);
Console.WriteLine(100 / value);
}
catch (FormatException)
{
Console.WriteLine("输入不是有效整数");
}
catch (DivideByZeroException)
{
Console.WriteLine("不能输入零");
}
⚠️ 注意: 不要用空的
catch 块吞掉异常,这会使问题难以排查。
不要吞掉异常
CSHARP
try
{
File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}
上面的代码隐藏了所有错误,是错误做法。至少应记录日志或向上抛出。
使用 finally 释放资源
CSHARP
FileStream fs = null;
try
{
fs = new FileStream("data.txt", FileMode.Open);
int b = fs.ReadByte();
}
finally
{
fs?.Dispose();
}
使用 using 语句替代 try-finally
实现了 IDisposable 接口的资源应优先使用 using 语句,它会在作用域结束时自动调用 Dispose,即使发生异常也能保证资源释放。
CSHARP
using (var fs = new FileStream("data.txt", FileMode.Open))
{
int b = fs.ReadByte();
}
📌 要点:
using 语句等价于 try-finally 加 Dispose() 调用,是资源管理的首选方式。
使用 InnerException 包装异常
CSHARP
try
{
SaveToFile();
}
catch (IOException ioEx)
{
throw new ApplicationException("保存数据失败", ioEx);
}
通过 InnerException 保留原始异常信息,不丢失上下文。
❓ 常见问题
Q
throw; 和 throw ex; 有什么区别?A
throw; 保留原始堆栈跟踪,throw ex; 将堆栈重置为当前方法,调试时应使用 throw;。Q
finally 块什么时候不会执行?A 当程序被强制终止(如
Environment.FailFast)或发生 StackOverflowException 时,finally 可能不会执行。Q 可以只有
try-finally 没有 catch 吗?A 可以。这种写法不处理异常,但保证资源清理,异常会继续向上传播。
Q 自定义异常类必须继承
Exception 吗?A 是的,所有异常类都必须继承自
Exception(直接或间接),才能被 catch 捕获。Q 异常筛选器
when 有什么优势?A
when 子句在不匹配时不会捕获异常,异常继续传播,比在 catch 内部判断更高效且语义更清晰。📖 小节
try块包裹可能出错的代码,catch捕获异常,finally始终执行- 常见异常类型包括
FormatException、NullReferenceException、IndexOutOfRangeException、DivideByZeroException等 Exception类提供Message、StackTrace、InnerException等属性throw;保留堆栈,throw ex;重置堆栈,应使用前者- 自定义异常类继承
Exception并实现标准构造函数 - 异常筛选器
catch ... when (...)提供条件化捕获能力 - 不要吞异常、捕获具体类型、用
using管理资源、用InnerException包装异常
📝 作业
- 编写一个方法
int SafeParse(string s),使用try-catch捕获FormatException,解析失败时返回0并打印警告信息 - 编写一个自定义异常
InvalidGradeException,包含Grade属性,当成绩不在 0-100 范围时抛出 - 编写代码演示
throw;与throw ex;的区别,分别打印两者的StackTrace并观察差异 - 使用异常筛选器
when实现:捕获FileNotFoundException时仅当文件路径以"config"开头时才处理,否则继续传播 - 编写一个文件读取方法,使用
using语句确保StreamReader正确释放,并处理可能出现的FileNotFoundException和IOException



