C# 异常处理

`try`/`catch`/`finally`、异常类型、`throw` 与自定义异常。

Intermediate
Key Points`finally` 一定会执行;`throw;` 保留原始堆栈,`throw ex;` 会重置它。

基本结构

C#
try
{
    int x = int.Parse("abc");
}
catch (FormatException ex)
{
    Console.WriteLine($"格式错误:{ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"其他错误:{ex.GetType().Name}");
}
finally
{
    Console.WriteLine("无论如何都执行");
}
Output
格式错误:The input string 'abc' was not in a correct format.
无论如何都执行

捕获顺序

C#
// 从具体到宽泛,否则编译错误(宽泛的会挡住具体的)
try
{
    object o = null!;
    _ = o.ToString();
}
catch (NullReferenceException ex)
{
    Console.WriteLine("空引用:" + ex.GetType().Name);
}
catch (Exception ex)
{
    Console.WriteLine("兜底:" + ex.GetType().Name);
}
Output
空引用:NullReferenceException

catch (Exception) must come last; otherwise later specific catches are unreachable and the compiler errors with CS0160 — a mistake C# prevents for you.

throw 的区别

C#
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]); }
Output
   at Program.RethrowBad() in Program.cs:line 3
   at Program.RethrowGood() in Program.cs:line 10
throw 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).

常见异常类型

ExceptionTrigger
ArgumentNullException参数为 null
ArgumentException参数不合法
ArgumentOutOfRangeException参数超出范围
InvalidOperationException当前状态不允许该操作
NullReferenceException访问 null 的成员
IndexOutOfRangeException数组越界
KeyNotFoundException字典键不存在
FormatException格式解析失败
IOException文件/网络 IO 失败
TimeoutException超时

自定义异常

C#
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}");
}
Output
余额不足:需要 500,可用 100
缺口 400

Derive from Exception, end the name with Exception, and provide three constructors (none, message, message+inner). Business fields like Required let the catcher react precisely.

什么时候不该用异常

C#
// 反例:用异常做流程控制,慢且难读
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).

异常过滤器

C#
try
{
    throw new IOException("磁盘已满");
}
catch (IOException ex) when (ex.Message.Contains("满"))
{
    Console.WriteLine("专门处理磁盘满");
}
catch (IOException ex)
{
    Console.WriteLine("其他 IO 错误");
}
Output
专门处理磁盘满

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.

Friends & Partners