基本
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# 路线图