基本

try
{
    DoRisky();
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"找不到: {ex.FileName}");
}
catch (IOException ex)        // 父异常类型
{
    Console.WriteLine($"IO 错误: {ex.Message}");
}
catch (Exception ex)          // 兜底
{
    Console.WriteLine($"其他错误: {ex}");
}
finally
{
    Cleanup();                // 无论成功失败都执行
}

catch 顺序:从具体到一般——子类异常在前,否则永远不会到达。

异常类层级

Exception
├── SystemException
│   ├── ArgumentException
│   │   ├── ArgumentNullException
│   │   └── ArgumentOutOfRangeException
│   ├── InvalidOperationException
│   ├── NullReferenceException
│   ├── IndexOutOfRangeException
│   ├── ArithmeticException
│   │   ├── DivideByZeroException
│   │   └── OverflowException
│   └── ... (几十个)
├── ApplicationException (历史包袱,不要用)
└── 自家异常

抛异常

public void Withdraw(decimal amount)
{
    if (amount <= 0)
        throw new ArgumentException("金额必须 > 0", nameof(amount));

    if (amount > Balance)
        throw new InvalidOperationException("余额不足");

    Balance -= amount;
}

nameof(amount) 比硬编码字符串好——重构时自动改。

throw 表达式

// C# 7+
var name = input ?? throw new ArgumentNullException(nameof(input));

// 三元里
return condition ? value : throw new InvalidOperationException();

// 表达式体方法
public string Get() => _data ?? throw new InvalidOperationException("not loaded");

ArgumentNullException.ThrowIfNull(.NET 6+)

public void Foo(string s, object o)
{
    ArgumentNullException.ThrowIfNull(s);
    ArgumentNullException.ThrowIfNull(o);

    // 旧写法
    // if (s is null) throw new ArgumentNullException(nameof(s));
}

少一行模板。.NET 7 起还有 ArgumentException.ThrowIfNullOrEmpty(s)ArgumentOutOfRangeException.ThrowIfNegative(n) 等。

using:自动 Dispose

// 老写法
using (var file = File.OpenRead("data.txt"))
{
    // 用 file
}   // 出块自动 Dispose(哪怕里面抛异常)

// C# 8+ 简化
using var file = File.OpenRead("data.txt");
// 用 file
// 出当前作用域时自动 Dispose

任何 IDisposable(文件、流、连接、socket、HttpClient)都该 using。

try-finally:手动清理

var lockTaken = false;
try
{
    Monitor.Enter(_lock, ref lockTaken);
    // 临界区
}
finally
{
    if (lockTaken) Monitor.Exit(_lock);
}

using 解决不了的(如锁、非 IDisposable 资源)用 try-finally。

自定义异常

public class InsufficientFundsException : Exception
{
    public decimal Available { get; }
    public decimal Requested { get; }

    public InsufficientFundsException(decimal available, decimal requested)
        : base($"余额 {available},请求 {requested}")
    {
        Available = available;
        Requested = requested;
    }
}

约定:

  • 继承 Exception(不是 ApplicationException)
  • 名字以 Exception 结尾
  • 提供至少 ()(string)(string, Exception) 构造(推荐)

重新抛

try
{
    DoSomething();
}
catch (Exception ex)
{
    log.Error(ex, "失败");
    throw;        // ✅ 保留原堆栈
    // throw ex;  // ❌ 重置堆栈,丢失原位置
}

throw; 重新抛出原异常(保留堆栈);throw ex; 重新抛出新异常(堆栈从这里开始)。

包装异常

try { ... }
catch (SqlException ex)
{
    throw new RepositoryException("查询失败", ex);   // 内部异常链
}

外层只看到自家异常类型,底层细节通过 InnerException 链保留。

catch when 过滤

try { ... }
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // 只处理 404
}
catch (HttpRequestException ex)
{
    // 其他 HTTP 错误
}

when 让 catch 条件更精细——比 catch + if + throw; 干净。

哪些异常不该 catch

// 这些通常表示 bug,应该让程序崩溃以便修
NullReferenceException        // 解决 null bug
ArgumentNullException         // 调用方传了 null
ArgumentException             // 参数有问题
InvalidOperationException     // 状态错了
IndexOutOfRangeException      // 数组越界
StackOverflowException        // 无法 catch
OutOfMemoryException          // 通常无法救

catch 应该是恢复 / 兜底用——bug 让它崩,崩了就修。

异步代码的异常

public async Task DoAsync()
{
    try
    {
        await SomeAsync();
    }
    catch (IOException ex)
    {
        // ✅ 能 catch
    }
}

await 让 Task 抛的异常像同步一样捕获。

但是:

async void Handler()   // ❌
{
    throw new Exception();    // 没人 await,异常飞到进程顶层 → 崩溃
}

async void 的异常无法外部捕获。坚决避免(除事件 handler 外)。

全局兜底

// Console / Worker
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
    log.Fatal(e.ExceptionObject as Exception, "未处理异常");
};

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    log.Error(e.Exception, "未观察的 Task 异常");
    e.SetObserved();
};

记录最后一线日志。ASP.NET Core 有内置 middleware(UseExceptionHandler)。

性能

异常的开销大——抓堆栈、构造对象、跨方法跳转。不要用异常做控制流

// ❌ 慢
try { return int.Parse(s); } catch { return 0; }

// ✅ 快
if (int.TryParse(s, out var n)) return n; return 0;

TryXxx 是 C# 习惯——预知会失败的操作用 Try 模式,真异常才抛。

→ 下一篇 C# 路线图