CodeGym /课程 /C# SELF /调用栈和自定义异常的创建

调用栈和自定义异常的创建

C# SELF
第 13 级 , 课程 3
可用

1. 认识调用栈(Call Stack)

上节课我们简单提过调用栈,这次来详细聊聊它。

想象一下:用户在应用里点了个按钮。这个按钮会触发OnClick()(“按钮被按下”)方法,然后它又会调用LoadData()(加载数据),再接着是ReadFromFile()(从文件读取)。

突然在ReadFromFile()里出错了——文件没找到。谁的锅?

为了搞清楚,程序会“逆着足迹”往回走:从ReadFromFile()LoadData()OnClick()。这条路就是调用栈——就像一摞盘子,最上面的先掉下来。

程序会沿着这摞盘子往下走,直到遇到合适的catch,但沿途所有finally都会执行——这样资源才能被释放、文件被关掉、东西都收拾好。

在编程里,调用栈就是个列表,记着谁调用了谁,这样出错时就能顺着“请求链”回头查找原因。

它是怎么工作的

程序运行时,每次方法(或函数)被调用,都会往调用栈(列表)里加一行。如果运行过程中抛出异常,.NET的Exception类会保存这个栈的信息:哪些方法、按什么顺序被调用,最后导致了错误。

2. 调用栈到底有啥用

调用栈——调试(debug)复杂bug时你的好基友。

  • 它不仅告诉你发生了啥,还会告诉你具体在哪谁干的
  • 有时候,看着栈你会惊讶:程序怎么能走到这一步(尤其是有人不小心传了个奇怪的参数)。

常见场景:假设你有个巨大的项目,方法互相调用,嵌套十几层。突然冒出个NullReferenceException,你根本不知道程序怎么走到这的。打开栈——一整条调用链都能看到,立马就知道该从哪查起。

例子:

class MyClass 
{
    public void MethodA()     { MethodB(); }
    
    public void MethodB()     { MethodC(); }
    
    public void MethodC()     {
        throw new Exception("错误!");
    }    

    public void Main() 
    {
        try 
        { 
            MethodA(); 
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }        
    }
}

3. 怎么写自定义异常

有时候标准异常不够用

.NET里有一堆标准异常(ArgumentNullExceptionInvalidOperationException等等),但有时候还不够:

  • 你的应用有自己的“游戏规则”:比如用户一次不能买超过10个商品,或者你的业务逻辑里金额不能为负数。
  • 你想把应用逻辑的错误和“系统”错误区分开。

这时候就想自己造一个——反正可以嘛!


using System;

// 自定义异常:用户没找到
public class UserNotFoundException : Exception
{
    // 基础构造函数
    public UserNotFoundException() : base("用户没找到。") { }

    // 带消息的构造函数
    public UserNotFoundException(string message) : base(message) { }

    // 带消息和内部异常的构造函数
    public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
自定义异常示例 UserNotFoundException

关于构造函数的简要说明

  • 无参数——用默认消息
  • 自定义消息——有时候想加点细节
  • 带内部异常(inner)——如果错误是“套娃”出来的,别丢了重要信息。

怎么用自定义异常

假设我们在任务管理应用里写个查找用户的方法:

using System;

public class UserService
{
    public string FindUserNameById(int userId)
    {
        // “查找”用户,没找到就抛异常
        if (userId != 42)
            throw new UserNotFoundException($"id为{userId}的用户没找到。");

        return "马西姆";
    }
}

主程序里:

// 在Main里
var service = new UserService();
try
{
    string name = service.FindUserNameById(17);
    Console.WriteLine("用户名: " + name);
}
catch (UserNotFoundException ex)
{
    Console.WriteLine("查找用户出问题: " + ex.Message);
    // 这里也能通过ex.StackTrace拿到调用栈
}

如果我们传的id不是42,就会得到:

查找用户出问题: id为17的用户没找到。

4. 为什么要写自定义异常

日志和错误分组

比如你的应用里有各种各样的错误,要分开处理。数据库错误要记成“致命”,用户错误要弹红字,网络错误要重试。按异常类型分组就是个好办法。

面向对象和继承

可以把你的领域错误组织成继承体系:

public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }

现在,只要catch住MyAppException,你就能处理所有领域相关的错误;如果想对“订单太大”有特殊反应——就catch最具体的那个。

5. 写自定义异常要注意啥

  • 别为了异常而异常
    只有在这些情况下才写自定义异常:
    • 真的能让代码更清晰
    • 调用方有可能catch它
    • 想给调用方多点信息(通过字段/属性)
  • 好习惯:序列化
    .NET的标准异常都支持序列化(比如要跨网络传递)。简单应用用不到,但“进阶”场景下——加上[Serializable]属性并实现序列化构造函数(见官方文档)。做练习可以不管,工作上——问问你们组长 :)
    序列化就是把对象变成方便存储或传输的格式(比如写到文件或发到网络)。我们以后会详细讲序列化。

6. 调用栈的细节:哪里容易搞混

调用栈只显示异常发生的路径。如果你catch了异常又抛了个新的,但没把“内部”异常传进去(就是没用Exception(string, Exception inner)构造函数),你可能会丢掉最初的错误信息。这叫“隐藏栈”。

不推荐:

try
{
    // 这里出错了
}
catch (Exception ex)
{
    throw new Exception("发生了未知错误。"); // 原来的栈信息没了!
}

推荐:

try
{
    // 这里出错了
}
catch (Exception ex)
{
    throw new Exception("发生了未知错误。", ex); // 保留原始栈信息!
}

这样StackTrace里既有最初的错误路径,也有你的新消息。

7. 实用建议和常见坑

  • 别用异常来控制普通逻辑(比如“跳出循环”——有更优雅的写法!)。
  • 只catch你需要的异常类型——总是catchException不太好(可能会“吞掉”你不该处理的错误)。
  • 遇到复杂bug一定要记录调用栈。这能帮你省下无数调试时间。
  • 用内部异常——这样不会丢掉最初的错误原因。
  • 让你的自定义异常描述清楚——这样一年后别人(包括你自己)也能明白为啥会报这个错。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION