C# 异常处理详解
一、什么是异常?
1.1 生活比喻
异常就是程序运行中出现的"意外情况"。
打个比喻:
你开车去上班——正常情况下,点火、挂挡、踩油门,顺利到达。
异常就是:车没油了、轮胎爆了、前方封路——这些都是正常路线之外的"意外"。
程序也一样:
- 正常情况:读文件、处理数据、保存结果
- 异常:文件不存在、网络断了、数据格式不对
异常不是 bug,而是可预期的意外情况。 你的任务不是让异常永远不发生,而是在发生时有合适的处理。
1.2 没有异常处理会怎样?
Console.Write("请输入一个数字: ");
string input = Console.ReadLine();
int number = int.Parse(input); // 如果用户输入 "abc",程序直接崩溃!
Console.WriteLine($"你输入的是: {number}");
用户输入 abc → int.Parse 炸了 → 程序崩溃 → Windows 弹错误框 → 用户体验极差。
1.3 有了异常处理会怎样?
try
{
Console.Write("请输入一个数字: ");
string input = Console.ReadLine();
int number = int.Parse(input);
Console.WriteLine($"你输入的是: {number}");
}
catch (FormatException)
{
Console.WriteLine("错误:请输入有效的数字!");
}
catch (Exception ex)
{
Console.WriteLine($"发生未知错误: {ex.Message}");
}
用户输入 abc → 捕获错误 → 友好提示 → 程序继续运行。
二、try-catch 基本结构
2.1 语法
try
{
// 可能出错的代码
}
catch (具体异常类型 变量名)
{
// 处理特定类型的异常
}
catch
{
// 处理所有其他异常(不推荐,太宽泛)
}
finally
{
// 无论是否出错,都会执行的代码(可选)
}
2.2 基本示例
try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[10]); // 数组越界!
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("数组索引超出范围了!");
Console.WriteLine($"详细: {ex.Message}");
}
finally
{
Console.WriteLine("这段代码总是执行,无论有没有错误");
}
输出:
数组索引超出范围了!
详细: Index was outside the bounds of the array.
这段代码总是执行,无论有没有错误
2.3 执行流程
正常情况:
try → 执行成功 → finally → 继续后面的代码
异常情况:
try → 某行出错 → 跳到 catch → finally → 继续后面的代码
没有 catch 的异常情况:
try → 某行出错 → finally → 异常向上抛出 → 程序可能崩溃
三、常见的异常类型
| 异常类型 | 什么时候发生 | 例子 |
|---|---|---|
NullReferenceException |
访问了 null 对象 | string s = null; s.Length; |
IndexOutOfRangeException |
数组索引越界 | arr[10] 但只有 5 个元素 |
DivideByZeroException |
整数除以 0 | int x = 5 / 0; |
FormatException |
字符串格式不对 | int.Parse("abc") |
InvalidCastException |
强制转换失败 | (string)obj 但 obj 是 int |
FileNotFoundException |
文件不存在 | File.ReadAllText("不存在的.txt") |
IOException |
输入输出错误 | 文件被占用、磁盘满等 |
ArgumentNullException |
参数是 null | 方法要求参数不能为 null |
ArgumentException |
参数不合法 | 传了无效的参数值 |
OverflowException |
数值溢出 | checked 上下文中溢出 |
StackOverflowException |
无限递归 | 方法一直调自己 |
OutOfMemoryException |
内存不足 | 创建巨型对象 |
3.1 演示几个常见异常
// 1. NullReferenceException
try
{
string name = null;
Console.WriteLine(name.Length);
}
catch (NullReferenceException ex)
{
Console.WriteLine($"空引用:{ex.Message}");
}
// 2. DivideByZeroException
try
{
int a = 10, b = 0;
Console.WriteLine(a / b);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"除零错误:{ex.Message}");
}
// 3. FormatException
try
{
int n = int.Parse("hello");
}
catch (FormatException ex)
{
Console.WriteLine($"格式错误:{ex.Message}");
}
四、多个 catch —— 分层捕获
4.1 从具体到宽泛
try
{
// 可能出多种错误的代码
string path = "data.txt";
string content = File.ReadAllText(path); // 可能 FileNotFoundException
int number = int.Parse(content); // 可能 FormatException
int result = 100 / number; // 可能 DivideByZeroException
Console.WriteLine(result);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"文件没找到: {ex.Message}");
}
catch (FormatException ex)
{
Console.WriteLine($"文件内容不是数字: {ex.Message}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"文件内容不能是 0: {ex.Message}");
}
catch (Exception ex) // 兜底——捕获上面没列出来的所有异常
{
Console.WriteLine($"发生未知错误: {ex.Message}");
}
关键规则:catch 的顺序必须从具体到宽泛。 Exception 是所有异常的父类,必须放最后。
// ❌ 错误顺序——最宽泛的 Exception 放第一个,后面的永远执行不到
try { }
catch (Exception ex) { } // 兜住了所有
catch (FormatException ex) { } // 永远不会执行!编译错误!
// ✅ 正确顺序——从具体到宽泛
try { }
catch (FormatException ex) { } // 先处理具体的
catch (Exception ex) { } // 再兜底
4.2 when 过滤条件(C# 6.0+)
try
{
int score = int.Parse(Console.ReadLine());
if (score < 0 || score > 100)
throw new ArgumentOutOfRangeException(nameof(score), "分数必须在 0~100 之间");
}
catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "score")
{
Console.WriteLine("分数参数无效");
}
catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "age")
{
Console.WriteLine("年龄参数无效");
}
// 同一个异常类型,不同 when 条件走不同分支
五、finally —— 一定会执行的收尾
5.1 finally 一定会执行
// finally 在以下情况都会执行:
// 1. try 正常结束
// 2. catch 捕获了异常
// 3. 异常没被捕获,但会先跑 finally 再向上抛
try
{
Console.WriteLine("1. try 开始");
// throw new Exception("出错!");
Console.WriteLine("2. try 结束");
}
catch (Exception ex)
{
Console.WriteLine($"3. catch: {ex.Message}");
}
finally
{
Console.WriteLine("4. finally——无论怎样我都会执行");
}
Console.WriteLine("5. 后续代码");
正常执行输出:
1. try 开始
2. try 结束
4. finally——无论怎样我都会执行
5. 后续代码
抛异常的输出:
1. try 开始
3. catch: 出错!
4. finally——无论怎样我都会执行
5. 后续代码
5.2 finally 的典型用途
// 场景:打开文件,操作,关闭文件
StreamReader reader = null;
try
{
reader = new StreamReader("data.txt");
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
catch (FileNotFoundException)
{
Console.WriteLine("文件不存在");
}
finally
{
// 无论成功还是失败,都要关闭文件
reader?.Close();
Console.WriteLine("文件已关闭");
}
5.3 更优雅的方式——using
// using 语句会自动调用 Dispose,等价于 try-finally
using (StreamReader reader = new StreamReader("data.txt"))
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
// 离开 using 块时自动关闭,无论是否有异常
六、throw —— 主动抛出异常
6.1 基本用法
// 业务逻辑中,遇到不合理的情况主动抛异常
static void EnrollStudent(int age)
{
if (age < 6)
throw new ArgumentException("年龄不能小于 6 岁", nameof(age));
if (age > 100)
throw new ArgumentException("年龄不合法", nameof(age));
Console.WriteLine($"录取成功,年龄 {age}");
}
// 调用
try
{
EnrollStudent(3);
}
catch (ArgumentException ex)
{
Console.WriteLine($"录取失败: {ex.Message}");
}
6.2 重新抛出异常
// 场景:记录日志后把异常继续往上抛
// ❌ 错误写法——重置了堆栈信息
try
{
DoSomething();
}
catch (Exception ex)
{
Log(ex);
throw ex; // ← 堆栈信息重置了!
}
// ✅ 正确写法——保留原始堆栈
try
{
DoSomething();
}
catch (Exception ex)
{
Log(ex);
throw; // ← 保留原始堆栈信息
}
throw 和 throw ex 的区别:
throw;—— 重新抛出当前异常,堆栈信息完整throw ex;—— 抛出这个异常对象,堆栈从这一行重新开始
6.3 抛出一个包含原始异常的新异常
try
{
ReadConfigFile();
}
catch (FileNotFoundException ex)
{
// 包装成更语义化的异常向上抛
throw new ApplicationException("配置文件读取失败,请检查 config.json", ex);
// ↑
// ex 作为 InnerException 保留原始信息
}
七、自定义异常
7.1 定义自己的异常类
// 自定义异常——通常只加几个构造函数即可
public class StudentNotFoundException : Exception
{
public int StudentId { get; }
public StudentNotFoundException() { }
public StudentNotFoundException(string message)
: base(message) { }
public StudentNotFoundException(string message, Exception inner)
: base(message, inner) { }
public StudentNotFoundException(int studentId)
: base($"未找到 ID 为 {studentId} 的学生")
{
StudentId = studentId;
}
}
// 使用
static Student FindStudent(int id)
{
// 假设从数据库找...
bool found = false;
if (!found)
throw new StudentNotFoundException(id);
return null;
}
try
{
Student s = FindStudent(999);
}
catch (StudentNotFoundException ex)
{
Console.WriteLine(ex.Message); // 未找到 ID 为 999 的学生
Console.WriteLine($"查找的ID: {ex.StudentId}"); // 999
}
7.2 异常命名规范
自定义异常必须以 Exception 结尾。如
StudentNotFoundException、InvalidOrderException。
八、Exception 对象的常用属性
try
{
int[] arr = { 1, 2, 3 };
Console.WriteLine(arr[10]);
}
catch (Exception ex)
{
Console.WriteLine($"消息: {ex.Message}"); // 简短描述
Console.WriteLine($"来源: {ex.Source}"); // 哪个程序集出的错
Console.WriteLine($"堆栈: \n{ex.StackTrace}"); // 调用链
Console.WriteLine($"目标: {ex.TargetSite}"); // 哪个方法出的错
Console.WriteLine($"内部异常: {ex.InnerException}"); // 包装的原始异常
Console.WriteLine($"帮助链接: {ex.HelpLink}"); // 帮助文档链接
}
输出示例:
消息: Index was outside the bounds of the array.
来源: System.Private.CoreLib
堆栈:
at Program.Main() in D:\Code\Program.cs:line 12
目标: Void Main()
内部异常:
帮助链接:
九、完整的异常处理示例
9.1 学生成绩管理系统
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
// 自定义异常
public class InvalidScoreException : Exception
{
public int Score { get; }
public InvalidScoreException(int score)
: base($"分数 {score} 不合法,必须在 0~100 之间")
{
Score = score;
}
}
public class StudentNotFoundException : Exception
{
public string Name { get; }
public StudentNotFoundException(string name)
: base($"未找到学生: {name}")
{
Name = name;
}
}
class StudentManager
{
private List<Student> _students = new List<Student>
{
new Student { Name = "张三", Score = 92 },
new Student { Name = "李四", Score = 85 },
new Student { Name = "王五", Score = 78 },
};
public class Student
{
public string Name;
public int Score;
}
// 添加学生——验证分数合法性
public void AddStudent(string name, int score)
{
if (score < 0 || score > 100)
throw new InvalidScoreException(score);
_students.Add(new Student { Name = name, Score = score });
Console.WriteLine($"添加成功: {name} - {score}分");
}
// 查询学生——找不到了抛自定义异常
public Student FindStudent(string name)
{
var student = _students.FirstOrDefault(s => s.Name == name);
if (student == null)
throw new StudentNotFoundException(name);
return student;
}
// 显示所有学生
public void ShowAll()
{
foreach (var s in _students)
Console.WriteLine($" {s.Name}: {s.Score}分");
}
// 保存到文件
public void SaveToFile(string path)
{
try
{
List<string> lines = _students
.Select(s => $"{s.Name},{s.Score}")
.ToList();
File.WriteAllLines(path, lines);
Console.WriteLine($"保存成功: {path}");
}
catch (IOException ex)
{
Console.WriteLine($"文件写入失败: {ex.Message}");
throw; // 保留原始堆栈再往上抛
}
}
}
class Program
{
static void Main()
{
var manager = new StudentManager();
Console.WriteLine("===== 学生成绩管理系统 =====\n");
// 1. 显示现有学生
Console.WriteLine("【现有学生】");
manager.ShowAll();
// 2. 测试添加学生——分数不合法
Console.WriteLine("\n【测试1:添加分数不合法的学生】");
try
{
manager.AddStudent("赵六", 150); // 150 分不合法
}
catch (InvalidScoreException ex)
{
Console.WriteLine($"添加失败:{ex.Message}");
Console.WriteLine($"你输入的分数是 {ex.Score}");
}
// 3. 测试添加学生——正常
Console.WriteLine("\n【测试2:正常添加学生】");
try
{
manager.AddStudent("赵六", 88);
}
catch (InvalidScoreException ex)
{
Console.WriteLine(ex.Message);
}
// 4. 测试查找学生——不存在
Console.WriteLine("\n【测试3:查找不存在的学生】");
try
{
var s = manager.FindStudent("孙七");
Console.WriteLine($"找到: {s.Name} - {s.Score}分");
}
catch (StudentNotFoundException ex)
{
Console.WriteLine($"查找失败:{ex.Message}");
}
// 5. 最终列表
Console.WriteLine("\n【最终学生列表】");
manager.ShowAll();
}
}
输出:
===== 学生成绩管理系统 =====
【现有学生】
张三: 92分
李四: 85分
王五: 78分
【测试1:添加分数不合法的学生】
添加失败:分数 150 不合法,必须在 0~100 之间
你输入的分数是 150
【测试2:正常添加学生】
添加成功: 赵六 - 88分
【测试3:查找不存在的学生】
查找失败:未找到学生: 孙七
【最终学生列表】
张三: 92分
李四: 85分
王五: 78分
赵六: 88分
十、常见易错点(避坑指南)
坑1:用 try-catch 当 if-else 用
// ❌ 错误:用异常控制正常流程
try
{
int age = int.Parse(input);
}
catch (FormatException)
{
age = 18; // 默认值
}
// ✅ 正确:先判断是否能转换
if (int.TryParse(input, out int age))
Console.WriteLine($"年龄: {age}");
else
Console.WriteLine("输入无效");
原则:能先判断的,不要等异常发生了再处理。异常有性能开销。
坑2:吞掉异常不做任何处理
// ❌ 最差的做法——什么都不做
try
{
DoSomething();
}
catch { } // 默默地吞了,出了问题完全不知道!
// ✅ 至少要记录日志
try
{
DoSomething();
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
// 或写到日志文件
throw; // 如果处理不了,向上抛
}
坑3:catch (Exception) 捕获得太早
// ❌ 过早捕获——后面代码依赖前面的结果
try
{
var data = LoadData();
ProcessData(data); // data 可能为 null!
}
catch (Exception) { }
// ✅ 让每块独立
try
{
var data = LoadData();
try
{
ProcessData(data);
}
catch (Exception ex)
{
Console.WriteLine($"处理失败: {ex.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"加载失败: {ex.Message}");
}
坑4:在 catch 块中抛新异常但不保留原始异常
// ❌ 丢失了原始错误信息
try { ReadFile(); }
catch (IOException ex)
{
throw new Exception("读取失败"); // 原始 IOException 没了!
}
// ✅ 把原始异常作为 InnerException
try { ReadFile(); }
catch (IOException ex)
{
throw new ApplicationException("读取失败", ex); // 原始异常保留在 InnerException
}
坑5:finally 中有 return
// ❌ finally 中的 return 会覆盖 try 中的 return
static int GetValue()
{
try
{
return 10;
}
finally
{
return 20; // ⚠️ 最终返回的是 20!try 的 return 10 被覆盖了
}
}
Console.WriteLine(GetValue()); // 输出: 20
// ✅ 不要在 finally 中使用 return
坑6:类型转换异常用 catch 而不用 as/is
// ❌ 用异常做类型判断
try
{
Student s = (Student)obj;
}
catch (InvalidCastException)
{
Console.WriteLine("不是 Student");
}
// ✅ 用 as 或 is
Student s = obj as Student;
if (s == null)
Console.WriteLine("不是 Student");
// 或
if (obj is Student student)
Console.WriteLine($"是 Student: {student.Name}");
坑7:在构造函数中抛异常时忘记 using
// using 语句展开后是 try-finally,能保证 Dispose
using (var resource = new SomeDisposableClass())
{
// 即使构造函数抛异常,Dispose 也会被调用
}
// 但手动写 try-finally 要注意,如果构造函数在 resource = new 时就抛了
// resource 还是 null
十一、总结
异常处理的核心结构
try
{
// 可能出错的代码
}
catch (SpecificException1 ex) when (条件) // 处理特定异常(可选)
{
// 处理逻辑
}
catch (SpecificException2 ex) // 可以多个 catch
{
// 处理逻辑
}
catch (Exception ex) // 兜底(可选)
{
// 通用处理
throw; // 处理不了的向上抛
}
finally // 收尾(可选)
{
// 无论如何都执行——关闭文件、释放资源
}
什么时候用哪种处理方式?
| 情况 | 做法 |
|---|---|
| 能预见到、能处理 | try-catch + 具体异常类型 |
| 能预先判断 | 用 if / TryParse / is 判断,不靠异常 |
| 处理不了 | catch 中 throw; 向上抛 |
| 需要加更多信息 | throw new XxxException("消息", ex) 保留原始异常 |
| 无论如何要释放资源 | finally 或 using |
| 业务逻辑不合法 | throw new 主动抛出 |
记忆口诀
try 块里写代码,可能出错的统统包
catch 匹配异常型,具体到宽泛顺着排
finally 最后跑,开过的资源要关掉
能判断的先判断,别靠异常做判断
处理不了往上抛,throw 不带 ex 保堆栈
using 写法最简洁,自动释放不闹心
一句话总结:异常是程序运行中的"意外情况",用
try-catch-finally来捕获和处理。关键是:能预判的就预判(TryParse、is),处理不了的向上抛(throw;),资源释放用finally或using,不要默默吞掉异常。