首页 / C# 入门教程 / 异常基础

C# 入门教程

异常基础

本教程共 100 篇 · 第 73 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程异常处理try-catchfinally

73. 异常基础

本节目标:学完你能在可能出错的代码外加 try/catch 兜底,用 finally 做清理,并理解异常会沿调用栈自动向上传递。

程序运行时总有”预料之外”:读了一个空字符串去转数字、数组下标越了界、除数是零。这些情况下 C# 会抛出一个异常(exception)。异常处理(exception handling)就是给程序系上”安全带”,让它在出错时不至于直接崩溃。

为什么需要异常处理

在没有异常机制的语言里,程序员通常用”返回错误码”来报告失败——每个方法多返回一个整数,0 代表成功,非 0 代表某种错误。问题在于:错误码很容易被调用方忽略,方法链一长,谁都忘了检查,错误就一路带着走到离源头很远的地方才爆发,那时早已无从查起。

C# 的异常机制反其道而行:一旦出错就”中断当前流程、一路向上冒”,除非有人显式接住。这逼着错误无法被悄悄忽略——要么你处理它,要么程序明确停下并给出堆栈。换句话说,异常让”出错了”这件事想藏都藏不住。

Note

异常(exception)不等于”错误(error)“。它是 C# 用来传递”运行期意外状况”的一个对象,本身也是普通类,携带消息、堆栈等诊断信息。

try / catch / finally 三件套

异常处理的三个关键字是 trycatchfinally

  1. try:把”可能出错”的代码包起来。
  2. catch:出错时接管,决定怎么处理。
  3. finally:不管出没出错,都一定会执行,常用来释放资源。
namespace CSharpDemo;

int Divide(int a, int b) => a / b;

try
{
    int result = Divide(10, 0);
    Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"出错:{ex.Message}");
}
finally
{
    Console.WriteLine("无论是否出错,这里都会执行");
}
Note

一旦 try 里抛了异常,控制权立刻跳到匹配的 catch,try 中剩余的代码不再执行。catch 处理完,再走 finally。

捕获具体的异常类型

catching 时要写清楚”捕获哪一类异常”。C# 里所有异常都源自 System.Exception,但更推荐捕获具体类型,比如除零是 DivideByZeroException、格式错误是 FormatException。

namespace CSharpDemo;

string input = "不是数字";
try
{
    int n = int.Parse(input);
    Console.WriteLine(n);
}
catch (FormatException)
{
    Console.WriteLine("输入的不是合法整数");
}
Tip

不要把所有错误都用一个 catch (Exception) 笼统吞掉。捕获具体类型,才能针对性处理,也更便于排查问题。

异常的传播:沿调用栈向上冒泡

实际项目里,异常往往不是直接抛在你写的 try 里,而是在更深的方法中。CLR 会沿着调用栈(call stack)一层层往上找能接住的 catch;找到就停下,找不到就让程序终止。

namespace CSharpDemo;

void Level3() => int.Parse("abc");   // 这里抛出 FormatException
void Level2() => Level3();
void Level1() => Level2();

try
{
    Level1();
}
catch (FormatException ex)
{
    Console.WriteLine($"在顶层捕获:{ex.Message}");
}

这段代码的异常从 Level3 冒到 Level2、Level1,最后被最外层的 catch 接住。这就是”栈展开(stack unwinding)”。

Note

如果一路上没有任何 catch 接住异常,程序会终止并把错误显示给用户。所以关键的入口处要有兜底处理。

Tip

正因为异常会自动向上冒泡,你不必在每一层都写 try/catch。只在”能真正处理这个错误的地方”去接——比如最外层给用户一个友好提示,中间层放手让它穿过。

finally:不管怎样都要收尾

有些动作必须执行,比如关掉打开的文件、释放连接。finally 就是干这个的——即使 try 里 return 了、抛异常了,它也会跑。

namespace CSharpDemo;

try
{
    Console.WriteLine("尝试打开资源");
    throw new InvalidOperationException("模拟故障");
}
finally
{
    Console.WriteLine("释放资源(一定会执行)");
}
Tip

现代 C# 更推荐用下一章要讲的 using 来管理资源,但 finally 仍是”必须执行收尾逻辑”的通用手段。

异常里有什么信息

每个异常对象都带着有用的诊断信息:Message 是文字描述,StackTrace 是出错时的调用路径。调试时看 StackTrace,能直接定位到哪一行炸了。

namespace CSharpDemo;

try
{
    int.Parse("x");
}
catch (Exception ex)
{
    Console.WriteLine($"消息:{ex.Message}");
    Console.WriteLine($"堆栈:{ex.StackTrace}");
}
Note

ex.StackTrace 记录的是”从抛出异常的那行,一路到被捕获的调用路径”。它是你排查线上问题的第一手线索,记日志时务必带上。

多个 catch:从具体到宽泛

一个 try 可以配多个 catch,把最具体的异常放最前面,最宽泛的(Exception)放最后。CLR 自上而下匹配第一个命中的。

namespace CSharpDemo;

try
{
    int.Parse(Console.ReadLine());
}
catch (FormatException)
{
    Console.WriteLine("格式不对");
}
catch (OverflowException)
{
    Console.WriteLine("数字太大或太小");
}
catch (Exception ex)
{
    Console.WriteLine($"其他错误:{ex.Message}");
}
Note

顺序写反(把 catch (Exception) 放最前)会编译报错——宽泛的必须殿后,否则后面的具体 catch 永远没机会执行。

异常过滤器:catch 时加 when(C# 6)

除了按顺序匹配类型,还可以用 when 在 catch 上加条件,只有条件成立才接住。这样能在”同一类型”里按细节分流处理。

namespace CSharpDemo;

try
{
    int.Parse("abc");
}
catch (FormatException ex) when (ex.Message.Contains("abc"))
{
    Console.WriteLine("是因为含字母导致的格式错误");
}
catch (FormatException)
{
    Console.WriteLine("其它格式错误");
}
Tip

when 过滤器的好处:异常对象本身不被”消耗”,匹配失败时还能继续往下找别的 catch。它比在 catch 内部再 if 判断更干净,也保留了原始堆栈上下文。

常见内置异常一览

异常触发场景
NullReferenceException对 null 调用成员
ArgumentNullException参数为 null(应主动抛)
ArgumentOutOfRangeException参数超出允许范围
FormatException字符串格式无法解析
IndexOutOfRangeException数组下标越界
InvalidOperationException对象状态不允许该操作
DivideByZeroException整数除以零
Warning

NullReferenceException(空引用)是最常见的崩溃来源。它往往意味着代码假设了”某个对象一定不是 null”,但实际是 null。配合第 90 章的可空引用类型(string? 等)能在编译期挡掉一大半这类问题。

catch 也可以不接变量

如果只关心”出错了要做什么”,不关心异常细节,catch 后面可以不写变量:

namespace CSharpDemo;

try { int.Parse("x"); }
catch { Console.WriteLine("发生错误"); }
Tip

没有变量时编译器仍按类型匹配。不过日常更推荐写具体类型或 catch (Exception),方便记日志。

finally 里别再抛异常

finally 的职责是”清理”,本身不应再抛新异常,否则会掩盖原始异常,让排错更乱。清理动作(如关闭文件)尽量简单、用 try 包好。

Warning

若 finally 里又抛了新异常,原始异常会被”顶掉”,你再也看不到最初出错的原因。清理代码务必轻量、稳当,必要时把清理本身也包一层保护。

与其它语言对比

  • 对比 C/C++ 的返回码:C# 异常不会被”忘记检查”而悄悄忽略,错误更显眼。
  • 对比 Java:两者异常体系相似,C# 不区分”受检(checked)/非受检”异常,写起来更轻,但也更靠自觉去处理。
  • 对比 Go:Go 用多返回值 (结果, error) 显式传递错误,C# 用异常中断流程。风格不同,没有对错,习惯即可。

常见疑问解答

问:要不要给每个方法都包 try/catch? 不要。过度捕获会让代码又臭又长,还容易把该暴露的问题藏起来。只在”能处理错误”或在”程序边界(入口、UI、接口)“做兜底。

问:catch 里什么也不写(空块)行不行? 强烈不建议。错误被静默吞掉,之后会在更远更诡异的地方爆发。真要忽略也要写注释说明原因,或至少记一行日志。

问:finally 和 return 谁先? finally 一定先执行完,方法才真正返回。即使在 try 里写了 return,finally 也会抢在返回之前跑。

最佳实践速记

  • try 只包”真正可能出错”的最小范围,别把整段逻辑都塞进去。
  • 优先捕获具体异常类型,宽泛的 Exception 放最后殿后。
  • finally 做必做的清理,别在里面抛新异常。
  • 别吞异常,别在每层都捕获。
  • 调试看 MessageStackTrace,记日志两者都要留。

小结

try 包住可能出错的代码,catch 按具体类型接管,finally 做必做的清理;异常会沿调用栈自动向上传播,无人接住就终止程序。下一章我们反过来学:什么时候该由你主动 throw 把异常抛出去。