C#异常处理完全指南
🔍 一、异常的核心概念
异常是什么?
异常 = 程序运行时的意外情况
就像开车时的突发事件(爆胎、没油、事故),需要特殊处理才能继续。
两种错误的对比:
| 错误类型 | 发生时间 | 特点 | 解决方法 |
|---|---|---|---|
| 编译错误 | 编译时(写代码时) | 语法错误、类型错误 | 修改代码 |
| 运行错误 | 运行时(程序执行时) | 数组越界、除零、空引用 | 异常处理 |
🎯 二、try-catch-finally 结构
基本结构(三段式)
csharp
try
{
// 尝试执行的代码(可能出错)
// 这里是"危险区域"
}
catch (异常类型 变量名)
{
// 捕获并处理异常
// 这里是"急救站"
}
finally
{
// 无论是否发生异常,都会执行
// 这里是"清理现场"
// 通常用于释放资源
}
📝 三、异常处理详细解析
1. try 块 - 尝试执行
csharp
try
{
// 把可能出错的代码放在这里
string input = Console.ReadLine();
int number = int.Parse(input); // 可能抛出 FormatException
int result = 100 / number; // 可能抛出 DivideByZeroException
}
2. catch 块 - 捕获处理
csharp
// 捕获特定异常
catch (FormatException ex)
{
Console.WriteLine("输入的不是有效数字!");
Console.WriteLine($"错误详情:{ex.Message}");
}
// 捕获多个异常(从具体到一般)
catch (DivideByZeroException)
{
Console.WriteLine("不能除以零!");
}
catch (Exception ex) // 通用异常(放在最后)
{
Console.WriteLine($"发生未知错误:{ex.Message}");
// 记录日志
LogError(ex);
}
3. finally 块 - 清理资源
csharp
FileStream file = null;
try
{
file = File.OpenRead("data.txt");
// 处理文件...
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在!");
}
finally
{
// 无论是否出错,都要关闭文件
if (file != null)
file.Close();
Console.WriteLine("资源已清理");
}
📊 四、常见异常类型速查表
| 异常类型 | 何时发生 | 示例 |
|---|---|---|
| FormatException | 格式转换失败 | int.Parse("abc") |
| OverflowException | 数值溢出 | int.MaxValue + 1 |
| DivideByZeroException | 除以零 | 10 / 0 |
| IndexOutOfRangeException | 数组越界 | arr[10](数组只有5个元素) |
| NullReferenceException | 空引用 | string s = null; s.Length; |
| FileNotFoundException | 文件不存在 | File.OpenRead("不存在的文件.txt") |
| IOException | 输入输出错误 | 磁盘满、无权限 |
| ArgumentException | 参数无效 | 传给方法的参数不符合要求 |
🔧 五、异常处理最佳实践
1. 具体的异常先捕获
csharp
try
{
// ...
}
catch (FormatException ex) // 具体的
{
// 处理格式错误
}
catch (Exception ex) // 一般的(放最后)
{
// 处理其他所有异常
}
2. 不要"吞掉"异常
csharp
// ❌ 错误:隐藏了错误
try
{
ProcessData();
}
catch (Exception)
{
// 什么都不做?用户不知道出错了!
}
// ✅ 正确:至少记录或通知
try
{
ProcessData();
}
catch (Exception ex)
{
LogError(ex); // 记录日志
ShowUserFriendlyMessage(); // 给用户友好提示
}
3. 使用 using 简化资源清理
csharp
// 传统方式
FileStream file = null;
try
{
file = File.OpenRead("data.txt");
// 使用文件
}
finally
{
file?.Close();
}
// 简化方式(推荐)
using (FileStream file = File.OpenRead("data.txt"))
{
// 使用文件
// 自动调用 file.Dispose(),相当于关闭
}
4. 抛出有意义的异常
csharp
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("取款金额必须大于0", nameof(amount));
if (amount > Balance)
throw new InvalidOperationException("余额不足");
// 正常逻辑
Balance -= amount;
}
⚠️ 六、避免的常见错误
1. 过度使用 try-catch
csharp
// ❌ 错误:用异常处理正常逻辑
try
{
if (array[index] != null) // 正常检查
{
// ...
}
}
catch (IndexOutOfRangeException) // 不应该用异常!
{
// ...
}
// ✅ 正确:用条件判断
if (index >= 0 && index < array.Length)
{
if (array[index] != null)
{
// ...
}
}
2. catch 块中再抛出不包装
csharp
// ❌ 错误:丢失原始异常信息
try
{
ProcessFile();
}
catch (Exception)
{
throw; // 直接重新抛出,可能丢失上下文
}
// ✅ 正确:包装异常,保留信息
try
{
ProcessFile();
}
catch (Exception ex)
{
throw new FileProcessingException("处理文件失败", ex);
}
3. 忽略 finally 的资源清理
csharp
// ❌ 错误:忘记清理
SqlConnection conn = new SqlConnection(connectionString);
try
{
conn.Open();
// 使用数据库...
}
catch (Exception ex)
{
// 处理异常,但连接没关闭!
}
// ✅ 正确:确保清理
SqlConnection conn = null;
try
{
conn = new SqlConnection(connectionString);
conn.Open();
// ...
}
finally
{
conn?.Close(); // 确保关闭
}
🛠️ 七、实用异常处理模式
模式1:重试机制
csharp
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries)
{
try
{
ConnectToService();
break; // 成功则退出循环
}
catch (TimeoutException)
{
retryCount++;
if (retryCount == maxRetries)
throw;
Console.WriteLine($"连接超时,第{retryCount}次重试...");
Thread.Sleep(1000); // 等待1秒后重试
}
}
模式2:验证输入
csharp
public int GetValidNumber()
{
while (true) // 直到输入有效
{
try
{
Console.Write("请输入一个数字:");
string input = Console.ReadLine();
return int.Parse(input);
}
catch (FormatException)
{
Console.WriteLine("输入的不是有效数字,请重新输入!");
}
catch (OverflowException)
{
Console.WriteLine("数字超出范围,请重新输入!");
}
}
}
模式3:资源安全访问
csharp
public string ReadFileSafely(string filePath)
{
try
{
return File.ReadAllText(filePath);
}
catch (FileNotFoundException)
{
return "文件不存在";
}
catch (UnauthorizedAccessException)
{
return "没有访问权限";
}
catch (IOException ex)
{
return $"读取文件失败:{ex.Message}";
}
catch (Exception ex)
{
LogError(ex); // 记录未知错误
return "系统错误";
}
}
📈 八、异常处理层次结构
C#异常继承体系
text
Object
└── Exception (所有异常的基类)
├── SystemException (系统异常)
│ ├── ArgumentException (参数异常)
│ ├── FormatException (格式异常)
│ ├── ArithmeticException (算术异常)
│ │ ├── DivideByZeroException (除零异常)
│ │ └── OverflowException (溢出异常)
│ ├── IndexOutOfRangeException (索引越界)
│ └── NullReferenceException (空引用)
└── ApplicationException (应用异常 - 自定义异常的基类)
自定义异常
csharp
public class InsufficientFundsException : Exception
{
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
public InsufficientFundsException(decimal current, decimal required)
: base($"余额不足。当前余额:{current},需要:{required}")
{
CurrentBalance = current;
RequiredAmount = required;
}
}
// 使用
if (amount > Balance)
throw new InsufficientFundsException(Balance, amount);
🎯 九、什么时候用异常处理?
应该用异常处理:
-
✅ 外部依赖可能失败(文件、网络、数据库)
-
✅ 用户输入不可控
-
✅ 资源访问需要清理
-
✅ 需要友好错误提示
不应该用异常处理:
-
❌ 控制正常程序流程
-
❌ 频繁发生的"正常"错误
-
❌ 性能关键路径
-
❌ 可以预防的错误(用条件检查)
💡 十、一句话总结
异常处理 = 给程序买保险
-
try:高风险操作 -
catch:出险理赔 -
finally:无论如何都要执行(像保险的必备条款) -
具体险种(异常类型)要明确,通用险(Exception)放最后
📋 十一、快速参考卡片
基本语法:
csharp
try
{
// 危险代码
}
catch (具体异常 ex)
{
// 处理具体异常
}
catch (Exception ex) // 通用,放最后
{
// 处理其他所有
// throw; // 重新抛出
}
finally
{
// 清理资源
}
常用方法:
csharp
ex.Message // 错误信息
ex.StackTrace // 调用堆栈
ex.InnerException // 内部异常
ex.GetType() // 异常类型
最佳实践:
-
具体异常先捕获
-
不要吞掉异常
-
使用using管理资源
-
finally确保清理
-
抛出有意义的异常
记住:好的异常处理让程序更健壮,坏的异常处理让bug更隐蔽!
递归算法核心概念精要
🔄 递归本质定义
递归 = 自我调用 + 问题分解 + 终止条件
函数直接或间接调用自身,将复杂问题分解为相同结构的子问题,直到达到可直接求解的基准情形。
🎯 递归三要素(黄金法则)
1. 基准条件(Base Case) ⭐
递归必须终止的条件,防止无限循环。
csharp
if (条件成立) return 结果;
2. 递归条件(Recursive Case) ⭐
问题如何分解为更小的相同问题。
csharp
return 当前结果 + 函数(更小问题);
3. 向基准条件收敛 ⭐
每次递归调用必须更接近基准条件。
🔄 递归执行过程
两个阶段:
-
递推阶段:不断深入调用,向基准条件推进
-
回归阶段:从基准条件开始,逐层返回结果
调用栈机制:
-
每次递归调用压入调用栈
-
返回时从栈顶弹出
-
深度过大导致栈溢出
⚖️ 递归 vs 迭代
| 维度 | 递归 | 迭代(循环) |
|---|---|---|
| 思维模型 | 自顶向下分解 | 自底向上构建 |
| 代码简洁性 | 更简洁直观 | 需要状态管理 |
| 性能开销 | 函数调用开销大 | 直接高效 |
| 内存使用 | 使用调用栈内存 | 通常更节省 |
| 适用问题 | 树状、分治、回溯 | 线性、序列处理 |
⚠️ 递归风险与限制
主要风险:
-
栈溢出:递归深度过大
-
重复计算:子问题重叠
-
性能低下:函数调用开销
-
难以调试:多层调用追踪困难
递归深度限制:
-
默认约 1000-3000 层
-
受系统栈大小限制
-
深度问题优先考虑迭代
🎯 递归适用场景判断
适合递归的特征:
-
问题可分解为相同子问题
-
有明显的基准情形
-
子问题相互独立(或可优化)
-
问题规模呈树状/层次结构
不适合递归:
-
深度无法控制
-
性能要求极高
-
已有简洁迭代解法
-
问题规模线性增长
🔧 递归优化策略
1. 尾递归优化
递归调用是函数最后操作,某些编译器可优化为迭代。
2. 记忆化(Memoization)
缓存已计算结果,避免重复计算。
3. 迭代转换
深度过大时转换为迭代算法。
💡 递归思维模式
关键转变:
从 "如何解决" 转为:
-
"基准情形是什么"
-
"如何分解为更小的相同问题"
-
"如何组合子问题的解"
思维验证:
-
是否所有路径都收敛到基准条件?
-
每次递归是否真正缩小问题规模?
-
是否存在重复计算?
-
深度是否可控?
📊 递归复杂度分析
时间复杂度公式:
text
T(n) = 递归调用次数 × 每次调用的时间复杂度
常见复杂度:
-
阶乘:O(n)
-
朴素斐波那契:O(2ⁿ) ⚠️
-
二分递归:O(log n)
-
树遍历:O(n)
🎯 递归设计原则
必须遵守:
-
明确基准条件 - 无终止则无限
-
确保收敛性 - 每次更接近基准
-
避免副作用 - 纯函数更安全
-
控制递归深度 - 预防栈溢出
推荐实践:
-
先定义基准条件
-
再思考递归分解
-
验证收敛性
-
考虑性能优化
💎 一句话精髓
递归 = 用定义解决问题
将问题定义为其自身的简化版本,加上明确的终止条件,通过自我调用的链条将复杂问题逐步简化至可直接求解。
记住:递归是一种思维方式,而非单纯的编程技巧。掌握递归的核心在于理解"如何将大问题分解为小问题"和"何时停止分解"这两个关键点。