CSharp异常处理学习笔记

一、什么是异常

异常,就是程序运行的时候出现的错误。

举个例子:你想把一个空字符串 "" 转成整数,编译器不会报错(语法没问题),但程序一跑起来就会出错。这种"运行时才暴露出来的错误",就是异常。

string str = "";
int val = int.Parse(str);   // 运行时这里会抛异常:输入字符串格式不正确

如果不对异常做处理,程序会直接中断退出,后面的代码都不会执行。

异常处理要解决的问题就是:程序出错了,但不要直接崩掉,而是由我们自己接管,做出合理的处理,让程序尽量保持正常运行。

二、异常处理的四个关键字

C# 的异常处理基于四个关键字:trycatchfinallythrow

关键字 作用
try 容易出错的代码包起来,后面紧跟一个或多个 catch
catch 捕获 try 块里抛出的异常,做对应处理
finally 无论有没有异常,都会执行的代码块
throw 手动抛出一个异常

可以把它们理解成一个流程:

try 里"试一下",出错了 catch "接住它",最后 finally "收尾(不管出没出错都收)"。

三、try-catch 语句

3.1 基本语法

try
{
    // 可能会引起异常的语句
}
catch (异常类型1 e1)
{
    // 处理异常类型1
}
catch (异常类型2 e2)
{
    // 处理异常类型2
}
finally
{
    // 可以没有 finally,也可以有
    // 不管有没有异常,都会执行
}

3.2 一个完整小例子

int val;
try
{
    string str = "";
    val = int.Parse(str);      // 这里会抛出异常
}
catch (Exception e)
{
    Console.WriteLine($"异常消息:{e.Message}");
}
finally
{
    val = -1;                  // 无论是否异常,最后都会把 val 设为 -1
}

Console.WriteLine($"val = {val}");   // 输出:val = -1

运行结果:

异常消息:输入字符串的格式不正确。
val = -1

注意看:虽然 int.Parse(str) 出错了,但程序没有崩溃catch 把异常接住了,finally 和后面的代码照常执行。

四、finally 的两个要点

  1. 无论如何都会执行:不管 try 里有没有抛异常,不管 catch 有没有接住,finally 都会跑。
  2. 即使 try 或 catch 里有 returnfinally 依然会先执行,再真正返回。
static int Test()
{
    try
    {
        return 1;
    }
    finally
    {
        Console.WriteLine("finally 执行了");
    }
}
// 先打印 "finally 执行了",再返回 1

finally 常用来做收尾工作:关闭文件、关闭数据库连接、释放资源等。

特殊说明:只有极少数情况 finally 不会执行,比如程序进程被强制杀死、Environment.Exit() 被调用、finally 块里自己又抛了异常等。面试题会考到,见后面。

五、C# 异常类的继承结构

所有异常都直接或间接派生自 System.Exception 类。

System.Exception
├── System.SystemException        ← 系统预定义异常的基类
│     ├── System.IO.IOException
│     ├── System.IndexOutOfRangeException
│     ├── System.NullReferenceException
│     ├── System.DivideByZeroException
│     └── ...(还有很多)
└── System.ApplicationException   ← 应用程序(自定义)异常的基类
      └── 我们自定义的异常类

两个重要分支:

  • System.SystemException:所有系统预定义异常的基类,比如除零、空引用、数组越界等。
  • System.ApplicationException:支持由应用程序自己产生的异常,所以我们自定义的异常应该继承这个类。

六、常见预定义异常类

异常类 描述
System.IO.IOException 处理 I/O 错误(读写文件等)
System.IndexOutOfRangeException 数组索引超出范围
System.ArrayTypeMismatchException 数组类型不匹配
System.NullReferenceException 引用了空对象(null)
System.DivideByZeroException 除以零
System.InvalidCastException 类型转换出错
System.OutOfMemoryException 内存不足
System.StackOverflowException 栈溢出

七、多个 catch 块的匹配顺序

一个 try 后面可以跟多个 catch,用来分别处理不同类型的异常。程序会从上往下找第一个匹配的 catch

int val;
int a = 12;
int b = 0;
try
{
    string str = "";
    val = int.Parse(str);      // 这里先抛 FormatException
    int count = a / b;         // 这行不会执行到
}
catch (FormatException e)
{
    Console.WriteLine($"格式异常:{e.Message}");
}
catch (DivideByZeroException e)
{
    Console.WriteLine($"除零异常:{e.Message}");
}

重要规则:子类(具体异常)必须写在父类(Exception)前面。

try { ... }
catch (FormatException e) { ... }   // 具体异常在前,正确
catch (Exception e) { ... }         // 兜底,放最后

如果反过来,把 catch (Exception e) 写在最前面,后面的 catch 就永远执行不到,编译器会直接报错。

八、throw 与异常的向上传递(重点)

8.1 手动抛出异常

if (b == 0)
{
    throw new DivideByZeroException("除数不能为 0");
}

8.2 异常在调用层面的处理原则

这是实战中非常重要的一个原则:

如果存在方法调用,异常捕获应该在"调用层面"进行;被调用的方法内部一般不捕获异常。即便方法内部不得不捕获,也不能把异常"消化掉",而要把异常继续向上抛,让上方的调用者能捕获到。

看一个对比例子。

错误做法:方法内部把异常"吃掉"了

static int Divide(int a, int b)
{
    try
    {
        return a / b;
    }
    catch (Exception e)
    {
        Console.WriteLine($"内部处理:{e.Message}");
        return 0;      // 把异常吞掉了,返回一个"假结果"
    }
}

// 调用处
int result = Divide(12, 0);
Console.WriteLine(result);   // 输出 0,调用者根本不知道出错了

问题:调用者拿到一个 0,误以为计算成功,实际上已经出错了,后续可能引发更大的问题。

正确做法:内部不捕获,或捕获后向上抛,由调用者统一处理

static int Divide(int a, int b)
{
    // 方法内部不捕获异常,让异常自然向上抛
    return a / b;
}

// 调用处统一处理
try
{
    int result = Divide(12, 0);
    Console.WriteLine(result);
}
catch (DivideByZeroException e)
{
    Console.WriteLine($"调用处捕获:{e.Message}");
}

如果方法内部不得不做点处理(比如记录日志),处理完要重新抛出

static int Divide(int a, int b)
{
    try
    {
        return a / b;
    }
    catch (DivideByZeroException e)
    {
        Console.WriteLine("记录日志:" + e.Message);
        throw;      // 重新抛出,让上层也能捕获到
    }
}

这里要特别区分两个写法:

  • throw; —— 重新抛出原来的异常保留原始堆栈信息,推荐。
  • throw e; —— 重新抛出时会重置堆栈信息,导致丢失异常最初发生的位置,不推荐。

这也是一个高频面试考点。

九、自定义异常

当系统自带的异常不够用,我们可以自己定义异常类。按照规范,自定义异常应继承 System.ApplicationException(也可以直接继承 Exception,但继承 ApplicationException 更符合语义)。

// 1. 定义自己的异常类
public class AgeException : ApplicationException
{
    public AgeException(string message) : base(message) { }
}

// 2. 使用自定义异常
static void SetAge(int age)
{
    if (age < 0 || age > 150)
    {
        throw new AgeException("年龄不合法,必须在 0 到 150 之间");
    }
}

// 3. 调用处捕获
try
{
    SetAge(200);
}
catch (AgeException e)
{
    Console.WriteLine($"自定义异常:{e.Message}");
}

十、一个完整的综合示例

把上面的知识点串起来,写一个能跑通的小程序:

using System;

class Program
{
    static void Main()
    {
        try
        {
            int[] arr = { 1, 2, 3 };
            Console.WriteLine(arr[10]);          // 数组越界
        }
        catch (IndexOutOfRangeException e)
        {
            Console.WriteLine($"捕获到数组越界:{e.Message}");
        }
        catch (Exception e)                       // 兜底
        {
            Console.WriteLine($"其他异常:{e.Message}");
        }
        finally
        {
            Console.WriteLine("无论出不出错,这行都会执行");
        }

        Console.WriteLine("程序正常结束,没有崩溃");
    }
}

运行结果:

捕获到数组越界:索引超出了数组界限。
无论出不出错,这行都会执行
程序正常结束,没有崩溃

十一、面试题

1. try-catch-finally 的执行顺序是怎样的?

:先执行 try 块中的代码。如果没有异常,执行完 try 后跳入 finally;如果有异常,则跳到第一个匹配的 catch 处理,处理完再执行 finallyfinally 是最后一步,保证一定会执行(特殊情况除外)。

2. finally 一定会执行吗?什么情况下不会执行?

:绝大多数情况下会执行,包括 trycatch 中有 return 时也会先执行 finally。以下情况不会执行:

  • 进程被强制终止(如 kill、断电、程序崩溃)。
  • 调用了 Environment.Exit() 强制退出程序。
  • trycatch 块中陷入了死循环。
  • finally 块自身抛出了异常(会中断 finally 的继续执行)。

3. 如果 try 块里有 return,finally 还会执行吗?返回的是哪个值?

finally 会执行,且在 return 真正返回之前执行。但如果 finally 里也写了 return,它会覆盖 try 里的返回值。所以一般不建议在 finally 中写 return

4. catch (Exception)catch(不带参数)有什么区别?

catch (Exception e) 捕获异常并用变量 e 保存,可以访问异常信息(如 e.Message);catch 不带参数同样能捕获所有异常,但拿不到异常对象,无法查看具体信息。两者都建议放在所有具体 catch 之后作为兜底。

5. 多个 catch 块的匹配规则是什么?顺序有要求吗?

:程序按 catch 的书写顺序,从上往下找第一个类型匹配的块执行。要求具体异常在前,通用异常(Exception)在后。如果把 catch (Exception e) 放在最前面,会编译报错,因为后面的 catch 永远不可达。

6. throw;throw e; 有什么区别?

  • throw; 重新抛出原始异常保留完整的堆栈信息,能定位到异常最初发生的位置。
  • throw e;重置堆栈信息,堆栈从当前这一行重新开始,丢失最初的异常发生位置,不利于排查问题。

因此重新抛出时推荐使用 throw;

7. 自定义异常类应该继承哪个类?如何实现?

:应继承 System.ApplicationException(该类的本意就是支持应用程序自定义异常;直接继承 Exception 也可以)。实现时提供构造方法,并调用基类构造:

public class MyException : ApplicationException
{
    public MyException(string message) : base(message) { }
}

使用时通过 throw new MyException("...") 抛出,用 catch (MyException e) 捕获。

8. 异常处理有性能开销吗?是不是应该到处都用 try-catch?

:有开销。抛出和捕获异常会涉及堆栈展开、异常对象创建等操作,比正常流程慢。因此不要滥用 try-catch,也不要靠异常来做正常的流程控制。正确的原则是:

  • 能提前判断的情况用 if 判断,比如除零前先判断 b != 0
  • 真正"无法预知、无法提前判断"的运行时错误才用 try-catch。
  • 捕获异常通常放在调用层面统一处理,而不是每个方法内部都包一层。

9. 方法内部捕获了异常,为什么调用处就捕获不到了?

:异常一旦被某个 catch 捕获并处理(没有重新抛出),就不会继续向上传播。所以如果方法内部把异常"吞掉"了,调用处自然就捕获不到。正确做法是:方法内部不捕获,或捕获后处理完用 throw; 重新向上抛,让调用层面统一捕获处理。

10. using 语句和异常处理有什么关系?

using 语句在编译后等价于 try-finally 结构,用来保证资源(如文件流、数据库连接)在使用完后被自动释放(调用 Dispose 方法),即使发生异常也能正确释放,从而避免资源泄漏。

posted @ 2026-08-15 14:01  春煦景明  阅读(3)  评论(0)    收藏  举报