C#: 异常处理

1. try/catch/finally 基本机制

C# 使用 trycatchfinally 三个关键字构建异常处理机制。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 块始终执行

(1) 多个 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.

2. 常见异常类型

异常类型 触发场景
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

3. 异常类层次结构

所有异常类的继承关系如下:

TEXT 📖 仅展示
Object
  └─ Exception
       └─ SystemException
            ├─ FormatException
            ├─ NullReferenceException
            ├─ IndexOutOfRangeException
            ├─ DivideByZeroException
            ├─ OverflowException
            ├─ FileNotFoundException
            └─ ArgumentException
                 └─ ArgumentNullException

4. 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()

5. 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')

(1) 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; 会将堆栈截断到当前方法,丢失原始异常位置信息。


6. 异常筛选器

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 📖 仅展示
捕获超时异常,准备重试

7. 自定义异常类

通过继承 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}");
}
逻辑代码 44 行(超过 40 行限制,仅展示)
TEXT 📖 仅展示
余额不足:当前余额 100,需要 200
余额:100,尝试取款:200

8. 异常处理最佳实践

(1) 捕获具体异常,不要只捕获 Exception

CSHARP
try
{
    int value = int.Parse(input);
    Console.WriteLine(100 / value);
}
catch (FormatException)
{
    Console.WriteLine("输入不是有效整数");
}
catch (DivideByZeroException)
{
    Console.WriteLine("不能输入零");
}
⚠️ 注意: 不要用空的 catch 块吞掉异常,这会使问题难以排查。

(2) 不要吞掉异常

CSHARP
try
{
    File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}

上面的代码隐藏了所有错误,是错误做法。至少应记录日志或向上抛出。

(3) 使用 finally 释放资源

CSHARP
FileStream fs = null;
try
{
    fs = new FileStream("data.txt", FileMode.Open);
    int b = fs.ReadByte();
}
finally
{
    fs?.Dispose();
}

(4) 使用 using 语句替代 try-finally

实现了 IDisposable 接口的资源应优先使用 using 语句,它会在作用域结束时自动调用 Dispose,即使发生异常也能保证资源释放。

CSHARP
using (var fs = new FileStream("data.txt", FileMode.Open))
{
    int b = fs.ReadByte();
}
📌 要点: using 语句等价于 try-finallyDispose() 调用,是资源管理的首选方式。

(5) 使用 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 内部判断更高效且语义更清晰。

📖 小节

📝 作业

  1. 编写一个方法 int SafeParse(string s),使用 try-catch 捕获 FormatException,解析失败时返回 0 并打印警告信息
  2. 编写一个自定义异常 InvalidGradeException,包含 Grade 属性,当成绩不在 0-100 范围时抛出
  3. 编写代码演示 throw;throw ex; 的区别,分别打印两者的 StackTrace 并观察差异
  4. 使用异常筛选器 when 实现:捕获 FileNotFoundException 时仅当文件路径以 "config" 开头时才处理,否则继续传播
  5. 编写一个文件读取方法,使用 using 语句确保 StreamReader 正确释放,并处理可能出现的 FileNotFoundExceptionIOException
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏