C# 异常处理
`try`/`catch`/`finally`、异常类型、`throw` 与自定义异常。
基本结构
try
{
int x = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine($"格式错误:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"其他错误:{ex.GetType().Name}");
}
finally
{
Console.WriteLine("无论如何都执行");
}格式错误:The input string 'abc' was not in a correct format.
无论如何都执行捕获顺序
// 从具体到宽泛,否则编译错误(宽泛的会挡住具体的)
try
{
object o = null!;
_ = o.ToString();
}
catch (NullReferenceException ex)
{
Console.WriteLine("空引用:" + ex.GetType().Name);
}
catch (Exception ex)
{
Console.WriteLine("兜底:" + ex.GetType().Name);
}空引用:NullReferenceExceptioncatch (Exception) must come last; otherwise later specific catches are unreachable and the compiler errors with CS0160 — a mistake C# prevents for you.
throw 的区别
void RethrowBad()
{
try { throw new InvalidOperationException("原始"); }
catch (Exception ex) { throw ex; } // 堆栈从这里重置
}
void RethrowGood()
{
try { throw new InvalidOperationException("原始"); }
catch { throw; } // 保留原始堆栈
}
try { RethrowBad(); } catch (Exception e) { Console.WriteLine(e.StackTrace!.Split('\n')[0]); }
try { RethrowGood(); } catch (Exception e) { Console.WriteLine(e.StackTrace!.Split('\n')[0]); } at Program.RethrowBad() in Program.cs:line 3
at Program.RethrowGood() in Program.cs:line 10throw ex; resets the stack trace origin to this line, losing the real throw site forever — fatal for production debugging. **Always throw;**; to add context, wrap: throw new XxxException("...", ex).常见异常类型
| Exception | Trigger |
|---|---|
ArgumentNullException | 参数为 null |
ArgumentException | 参数不合法 |
ArgumentOutOfRangeException | 参数超出范围 |
InvalidOperationException | 当前状态不允许该操作 |
NullReferenceException | 访问 null 的成员 |
IndexOutOfRangeException | 数组越界 |
KeyNotFoundException | 字典键不存在 |
FormatException | 格式解析失败 |
IOException | 文件/网络 IO 失败 |
TimeoutException | 超时 |
自定义异常
class InsufficientBalanceException : Exception
{
public decimal Required { get; }
public decimal Available { get; }
public InsufficientBalanceException(decimal required, decimal available)
: base($"余额不足:需要 {required},可用 {available}")
{
Required = required;
Available = available;
}
}
void Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
throw new InsufficientBalanceException(amount, balance);
}
try
{
Withdraw(100, 500);
}
catch (InsufficientBalanceException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"缺口 {ex.Required - ex.Available}");
}余额不足:需要 500,可用 100
缺口 400Derive from Exception, end the name with Exception, and provide three constructors (none, message, message+inner). Business fields like Required let the catcher react precisely.
什么时候不该用异常
// 反例:用异常做流程控制,慢且难读
try
{
var n = int.Parse(input);
Console.WriteLine(n);
}
catch (FormatException)
{
Console.WriteLine("不是数字");
}
// 正例:TryParse
if (int.TryParse(input, out var value))
{
Console.WriteLine(value);
}
else
{
Console.WriteLine("不是数字");
}Exceptions are expensive in .NET: capturing a stack trace and unwinding costs microseconds to tens of microseconds. Use return values for expected failures (parse, validate, not-found) and exceptions for unexpected ones (vanished file, dropped network, full disk).
异常过滤器
try
{
throw new IOException("磁盘已满");
}
catch (IOException ex) when (ex.Message.Contains("满"))
{
Console.WriteLine("专门处理磁盘满");
}
catch (IOException ex)
{
Console.WriteLine("其他 IO 错误");
}专门处理磁盘满A when filter beats an if inside catch: the stack is not unwound when it does not match, which helps both debugging and performance. It also allows log-and-pass-through.
基本结构
try
{
int x = int.Parse("abc");
}
catch (FormatException ex)
{
Console.WriteLine($"格式错误:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"其他错误:{ex.GetType().Name}");
}
finally
{
Console.WriteLine("无论如何都执行");
}格式错误:The input string 'abc' was not in a correct format.
无论如何都执行捕获顺序
// 从具体到宽泛,否则编译错误(宽泛的会挡住具体的)
try
{
object o = null!;
_ = o.ToString();
}
catch (NullReferenceException ex)
{
Console.WriteLine("空引用:" + ex.GetType().Name);
}
catch (Exception ex)
{
Console.WriteLine("兜底:" + ex.GetType().Name);
}空引用:NullReferenceExceptioncatch (Exception) 放最后,否则后面的具体 catch 永远到不了——编译器会直接报错 CS0160。这是 C# 帮你避免的一个常见错误。
throw 的区别
void RethrowBad()
{
try { throw new InvalidOperationException("原始"); }
catch (Exception ex) { throw ex; } // 堆栈从这里重置
}
void RethrowGood()
{
try { throw new InvalidOperationException("原始"); }
catch { throw; } // 保留原始堆栈
}
try { RethrowBad(); } catch (Exception e) { Console.WriteLine(e.StackTrace!.Split('\n')[0]); }
try { RethrowGood(); } catch (Exception e) { Console.WriteLine(e.StackTrace!.Split('\n')[0]); } at Program.RethrowBad() in Program.cs:line 3
at Program.RethrowGood() in Program.cs:line 10throw ex; 会把堆栈起点改成当前位置,原始抛出点永久丢失。这在生产排查里是致命的——**永远用 throw;**。要加信息就包一层 throw new XxxException("...", ex)。常见异常类型
| 异常 | 触发场景 |
|---|---|
ArgumentNullException | 参数为 null |
ArgumentException | 参数不合法 |
ArgumentOutOfRangeException | 参数超出范围 |
InvalidOperationException | 当前状态不允许该操作 |
NullReferenceException | 访问 null 的成员 |
IndexOutOfRangeException | 数组越界 |
KeyNotFoundException | 字典键不存在 |
FormatException | 格式解析失败 |
IOException | 文件/网络 IO 失败 |
TimeoutException | 超时 |
自定义异常
class InsufficientBalanceException : Exception
{
public decimal Required { get; }
public decimal Available { get; }
public InsufficientBalanceException(decimal required, decimal available)
: base($"余额不足:需要 {required},可用 {available}")
{
Required = required;
Available = available;
}
}
void Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
throw new InsufficientBalanceException(amount, balance);
}
try
{
Withdraw(100, 500);
}
catch (InsufficientBalanceException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"缺口 {ex.Required - ex.Available}");
}余额不足:需要 500,可用 100
缺口 400自定义异常要继承 Exception,类名以 Exception 结尾,并至少提供三个构造函数(无参、message、message+inner)。带业务字段(如 Required)能让 catch 方做精确处理。
什么时候不该用异常
// 反例:用异常做流程控制,慢且难读
try
{
var n = int.Parse(input);
Console.WriteLine(n);
}
catch (FormatException)
{
Console.WriteLine("不是数字");
}
// 正例:TryParse
if (int.TryParse(input, out var value))
{
Console.WriteLine(value);
}
else
{
Console.WriteLine("不是数字");
}异常在 .NET 里是昂贵的:要捕获堆栈、展开栈帧,一次几微秒到几十微秒。可预期的失败(解析、校验、查找不到)用返回值表达;意外的失败(文件消失、网络断、磁盘满)才用异常。
异常过滤器
try
{
throw new IOException("磁盘已满");
}
catch (IOException ex) when (ex.Message.Contains("满"))
{
Console.WriteLine("专门处理磁盘满");
}
catch (IOException ex)
{
Console.WriteLine("其他 IO 错误");
}专门处理磁盘满when 过滤器比在 catch 里写 if 更好:不匹配时堆栈不被展开,调试体验和性能都更好。它还能用于「记录日志后不处理」这种场景。