1. 入门
想象一下,你在写一个两个数相除的计算器。所有功能都很棒,直到有人想拿0当除数:
int a = 10;
int b = 0;
int result = a / b; // 炸了!
这时候程序直接“崩了”,用户看到一堆吓人的报错。这就是异常情况 —— 程序正常流程里本不该发生的事,需要特别关注。
异常 —— 是一种特殊机制,.NET用它告诉你的程序:有啥不对劲,执行要么中断要么得特殊处理。
当运行时出错(比如除以零、访问不存在的文件,或者老朋友——解引用null),.NET会“抛出”(throw)一个特殊的异常对象。然后它会去找能“接住”(catch)这个异常并知道怎么处理的地方。
为啥不用“错误码”返回?
当然可以用错误码返回——很多上古语言(Pascal、C,还有一些现代API)就是这么干的。但这样很麻烦也很危险:很容易忘了检查错误码(毕竟大家都希望一切顺利),而且很难定位到底哪儿出错了。异常机制能让你集中处理各种错误,灵活应对,不用在主流程里塞一堆if判断。
2. “抛出”异常
异常可以自动发生——比如运行时出错,也可以用关键字throw手动抛出来:
int[] arr = new int[2];
arr[10] = 5; // 异常会自动抛出 System.IndexOutOfRangeException
throw new Exception("绝对灾难!"); // 手动抛出Exception异常
就算你觉得自己程序写得天衣无缝,也总有意外:用户关了文件、网络断了、有人拔了电脑电源(差不多吧)。
好在.NET不光给了我们“抛异常”的机制,还给了捕获和处理异常的工具——我们马上就会用到。
3. 异常的生命周期:从throw到catch
当代码里发生了啥糟心事,.NET会启动“紧急流程”:
- 创建异常对象(比如IndexOutOfRangeException)。
- 抛出它,用关键字throw。
- 找处理器:从调用栈底往上找(从当前方法到上层调用者)——找第一个类型匹配的catch块。
- 找到处理器——程序会在catch块里继续执行。
- 如果没找到处理器——程序直接崩溃,输出“调用栈”和错误细节。
就像球在楼梯上弹,没遇到接球的就一直往上蹦。
4. .NET里的异常继承体系
“父子树”结构
在.NET(和大多数OOP语言)里,异常都是类,互相继承,组成一棵树。
它们都有一个共同的祖先——基类System.Exception。
System.Object
└─ System.Exception
├─ System.SystemException
│ ├─ System.NullReferenceException
│ ├─ System.IndexOutOfRangeException
│ ├─ System.DivideByZeroException
│ ├─ System.OutOfMemoryException
│ └─ ... 还有很多
├─ System.IO.IOException
│ ├─ System.IO.FileNotFoundException
│ ├─ System.IO.DirectoryNotFoundException
│ └─ ...
├─ System.ArgumentException
│ ├─ System.ArgumentNullException
│ └─ System.ArgumentOutOfRangeException
└─ (你自定义的Exception类)
为啥用类?
有了类的继承体系,你可以一次性捕获一大类问题——比如所有参数相关的错误(ArgumentException及其子类)。而且还能自定义自己的异常类型(后面几节会讲)。
.NET最常见的异常
- NullReferenceException —— 访问等于null的对象的方法或属性。老朋友!
- DivideByZeroException —— 除以零。
- IndexOutOfRangeException —— 数组越界。
- ArgumentException、ArgumentNullException、ArgumentOutOfRangeException —— 方法参数有问题。
- FileNotFoundException、IOException —— 文件操作出错。
- InvalidOperationException —— 当前对象状态下不能操作。
- FormatException —— 数据格式不对(比如想把"abcd"转成数字)。
5. 示例:各种异常是怎么“炸”的
来点代码例子,直观感受下不同异常是怎么触发的。
例子1:NullReferenceException
string? str = null;
Console.WriteLine(str.Length); // 这里“炸了!”——str是null
输出:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
例子2:DivideByZeroException
int a = 42;
int b = 0;
int c = a / b; // 危险!除以0
输出:
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
例子3:IndexOutOfRangeException
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // 没有下标为5的元素
这里明显下标越界,输出如下:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
例子4:FileNotFoundException
using System.IO;
string content = File.ReadAllText("secret.txt");
如果你没有"secret.txt"这个文件,会看到类似这样的提示:
Unhandled exception. System.IO.FileNotFoundException: Could not find file '/Users/zapp/RiderProjects/ConsoleApp1/ConsoleApp1/bin/Debug/net9.0/secret.txt'.
例子5:FormatException
string input = "十三";
int number = int.Parse(input); // 字符串不是数字
输出:
Unhandled exception. System.FormatException: The input string '十三' was not in a correct format.
6. 怎么用好这些知识?
理解异常机制对每个开发者都很重要。这不是“理论为了理论”,而是直接影响你程序稳定性的实用技能。你知道怎么正确处理错误后,程序就不会一出错就崩,而是能友好地告诉用户哪里不对。这样程序更靠谱、更专业,也更有人情味。
而且这不只是为了方便。面试时经常会问异常处理机制、各种Exception类型,.NET里这个机制到底怎么运作。所以理解这块内容,对找工作也是加分项。
更重要的是,你以后在实际项目里肯定会遇到异常。文件操作、网络、数据库、第三方库和框架——全都离不开异常系统。早点搞明白,后面就不会被坑。
7. 用异常时常见的坑
坑1:完全无视异常。
有些新手(甚至老手)觉得异常很烦,要么直接无视,要么更离谱,把代码包在catch { }里啥也不干。结果就是程序“悄悄”继续跑,状态乱七八糟,根本查不到哪里出问题。
坑2:把异常和普通错误搞混。
很多人忘了,不是所有错误都要用try-catch。比如用户输入不合法,最好先自己检查,而不是指望FormatException,更别把异常当成业务逻辑。异常是特殊情况,不是日常流程。
坑3:没搞懂异常继承体系。
有人喜欢捕获太泛的类(Exception),结果啥都抓,包括本不该处理的错误。有的人又只抓很窄的类型(IndexOutOfRangeException、NullReferenceException),忘了还有别的异常。一定要理解.NET异常体系,知道自己到底想处理啥。
最重要的——记住: C#里的异常不是灾难,也不是“完蛋了”的信号,而是切换到特殊流程的机制。用对了,它就是你的神器。
GO TO FULL VERSION