目 录CONTENT

文章目录

CSharp(五十四) 异常处理详解

C# 异常处理详解


一、什么是异常?

1.1 生活比喻

异常就是程序运行中出现的"意外情况"。

打个比喻:

你开车去上班——正常情况下,点火、挂挡、踩油门,顺利到达。
异常就是:车没油了、轮胎爆了、前方封路——这些都是正常路线之外的"意外"。

程序也一样:

  • 正常情况:读文件、处理数据、保存结果
  • 异常:文件不存在、网络断了、数据格式不对

异常不是 bug,而是可预期的意外情况。 你的任务不是让异常永远不发生,而是在发生时有合适的处理。

1.2 没有异常处理会怎样?

Console.Write("请输入一个数字: ");
string input = Console.ReadLine();
int number = int.Parse(input);    // 如果用户输入 "abc",程序直接崩溃!
Console.WriteLine($"你输入的是: {number}");

用户输入 abcint.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 结尾。如 StudentNotFoundExceptionInvalidOrderException


八、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 判断,不靠异常
处理不了 catchthrow; 向上抛
需要加更多信息 throw new XxxException("消息", ex) 保留原始异常
无论如何要释放资源 finallyusing
业务逻辑不合法 throw new 主动抛出

记忆口诀

try 块里写代码,可能出错的统统包
catch 匹配异常型,具体到宽泛顺着排
finally 最后跑,开过的资源要关掉

能判断的先判断,别靠异常做判断
处理不了往上抛,throw 不带 ex 保堆栈
using 写法最简洁,自动释放不闹心

一句话总结:异常是程序运行中的"意外情况",用 try-catch-finally 来捕获和处理。关键是:能预判的就预判(TryParseis),处理不了的向上抛(throw;),资源释放用 finallyusing,不要默默吞掉异常。

0
博主关闭了当前页面的评论