C#异常处理18条最佳实践

2023-11-07

首先,异常处理应该是系统设计规约的一部分出现在系统设计文档中,而不仅仅是一种技术实现。

作为设计文档的一部分,异常处理应该着眼于系统容错性和稳定性(正如楼主提到的那样)。然后在根据这个规约,再来具体讨论和选择异常处理中使用的各种技术细则。
比如,在设计服务时,必须在服务的调用接口处有异常处理,否则客户端传过来的任何有害数据都可能让服务器挂掉。

比如,对异常的处理在系统的设计中,必须有明确说明,不能随便在哪个模块中处理异常。

软件中有bug是可以理解的。但是如果是经常出现的bug,并且因为没有足够的提示信息导致你不能迅速修复它,那么这种情况是不可被原谅的。

为了更好地理解我上面所说的话,我举个例子:我经常看见无数的商业软件在遇到硬盘不足时给出这样的错误提示:

“更新客户资料失败,请与系统管理员联系然后重试”。

除了这些外,其他任何信息都没有被记录。要搞清楚到底什么原因引起的这个错误是一件非常耗时的过程,在真正找到问题原因之前,程序员可能需要做各种各样的猜测。

下面就来说说C#异常处理18条最佳实践

1. 不要抛出“new Exception()”

请别这样做。Exception是一个非常抽象的异常类,捕获这类异常通常会产生很多负面影响。通常情况下应该定义我们自己的异常类,并且需要区分系统(framework)抛出的异常和我们自己抛出的异常。

2. 不要将重要的异常信息存储在Message属性中

异常都封装在类中。当你需要返回异常信息时,请将信息存储在一些单独的属性中(而不要放在Message属性中),否则人们很难从Message属 性中解析出他们需要的信息。比如当你仅仅需要纠正一下拼写错误,如果你将错误信息和其它提示内容一起以String的形式写在了Message属性中,那 么别人该怎样简单地获取他们要的错误信息呢?你很难想象到他们要做多少努力。

3. 每个线程要包含一个try/catch块

一般异常处理都放在了程序中一个比较集中的地方。每个线程都需要有一个try/catch块,否则你会漏掉某些异常从而出现难以理解的问题。当一个 程序开启了多个线程去处理后台任务时,通常你会创建一个类型来存储各个线程执行的结果。这时候请不要忘记了为类型增加一个字段来存储每个线程可能发生的异 常,否则的话,主线程不会知道其他线程的异常情况。在一些“即发即忘”的场合(意思主线程开启线程后不再关心线程的运行情况,译者注),你可能需要将主线 程中的异常处理逻辑复制一份到你的子线程中去。

4. 捕获异常后要记录下来

不管你的程序是使用何种方式记录日志——log4net、EIF、Event Log、TraceListeners或者文本文件等,这些都不重要。重要的是:当你遇到异常后,应该在某个地方将它记录在日志中。但是请仅仅记录一次, 否则的话,你最后会得到一个非常大的日志文件,包含了许多重复信息。

5. 不要只记录Exception.Message的值,还需要记录Exception.ToString()

当我们谈到记录日志时,不要忘了我们应该记录Exception.ToString()的值,而不是Exception.Message。因为 Exception.ToString()包含了“堆栈跟踪”(stack trace)信息,内部异常信息以及Message。通常这些信息非常重要,而如果你只记录Exception.Message的话,你只可能看到类似 “对象引用未指向堆中实例”这样的提示。

6. 要捕获具体的异常

如果你要捕获异常,请尽可能的捕获具体异常(而非Exception)。

我经常看见初学者说,一段好的代码就是不能抛出异常的代码。其实这说法是错误的,好的代码在必要时应该抛出相应的异常,并且好的代码只能捕获它知道该怎么处理的异常(注意这句话,译者注)。

下面的代码作为对这条规则的说明。我敢打赌编写下面这段代码的那个家伙看见了会杀了我的,但是它确实是摘取自真实编程工作中的一段代码。

第一个类MyClass在一个程序集中,第二个类GenericLibrary在另一个程序集中。在开发的机器上运行正常,但是在测试机器上却总是抛出“数据不合法!”的异常,尽管每次输入的数据都是合法的。

你们能说说这是为什么吗?

public class MyClass
{
    public static string ValidateNumber(string userInput)
    {
        try
        {
            int val = GenericLibrary.ConvertToInt(userInput);
            return "Valid number";
        }
        catch (Exception)
        {
            return "Invalid number";
        }
    }
}
 
public class GenericLibrary
{
    public static int ConvertToInt(string userInput)
    {
        return Convert.ToInt32(userInput);
    }
}

这个问题的原因就是异常处理不太具体。根据MSDN上的介绍,Convert.ToInt32方法仅仅会抛出ArgumentException、FormatException以及OverflowException三个异常。所以,我们应该仅仅处理这三个异常。

问题发生在我们程序安装的步骤上,我们没有将第二个程序集(GenericLibrary.dll)打包进去。所以程序运行 后,ConvertToInt方法会抛出FileNotFoundException异常,但是我们捕获的异常是Exception,所以会提示“数据不 合法”。

7. 不要中止异常上抛

最坏的情况是,你编写catch(Exception)这样的代码,并且在catch块中啥也不干。请不要这样做。

8. 清理代码要放在finally块中

大多数时候,我们只处理某一些特定的异常,其它异常不负责处理。那么我们的代码中就应该多一些finally块(就算发生了不处理的异常,也可以在finally块中做一些事情,译者注),比如清理资源的代码、关闭流或者回复状态等。请把这当作习惯。

有一件大家容易忽略的事情是:怎样让我们的try/catch块同时具备易读性和健壮性。举个例子,假设你需要从一个临时文件中读取数据并且返回一个字符串。无论什么情况发生,我们都得删除这个临时文件,因为它是临时性的。

让我们先看看最简单的不使用try/catch块的代码:

string ReadTempFile(string FileName)
{
    string fileContents;
    using (StreamReader sr = new StreamReader(FileName))
    {
        fileContents = sr.ReadToEnd();
    }
    File.Delete(FileName);
    return fileContents;
}

这段代码有一个问题,ReadToEnd方法有可能抛出异常,那么临时文件就无法删除了。所以有些人修改代码为:

string ReadTempFile(string FileName)
{
    try
    {
        string fileContents;
        using (StreamReader sr = new StreamReader(FileName))
        {
            fileContents = sr.ReadToEnd();
        }
        File.Delete(FileName);
        return fileContents;
    }
    catch (Exception)
    {
        File.Delete(FileName);
        throw;
    }
}

这段代码变得复杂一些,并且它包含了重复性的代码。

那么现在让我们看看更简介更健壮的使用try/finally的方式:

string ReadTempFile(string FileName)
{
    try
    {
        using (StreamReader sr = new StreamReader(FileName))
        {
            return sr.ReadToEnd();
        }
    }
    finally
    {
        File.Delete(FileName);
    }
}

变量fileContents去哪里了?它不再需要了,因为返回点在清理代码前面。这是让代码在方法返回后才执行的好处:你可以清理那些返回语句需要用到的资源(方法返回时需要用到的资源,所以资源只能在方法返回后才能释放,译者注)。

9. 不要忘记使用using

仅仅调用对象的Dispose()方法是不够的。即使异常发生时,using关键字也能够防止资源泄漏。

10.不要使用特殊返回值去表示方法中发生的异常

因为这样做有很多问题:

1)直接抛出异常更快,因为使用特殊的返回值表示异常时,我们每次调用完方法时,都需要去检查返回结果,并且这至少要多占用一个寄存器。降低代码运行速度。

2)特殊返回值能,并且很可能被忽略

3)特殊返回值不能包含堆栈跟踪(stack trace)信息,不能返回异常的详细信息

4)很多时候,不存在一个特殊值去表示方法中发生的异常,比如,除数为零的情况:

public int divide(int x, int y)
{
    return x / y;
}

11. 不要使用“抛出异常”的方式去表示资源不存在

微软建议在某些特定场合,方法可以通过返回一些特定值来表示方法在执行过程中发生了预计之外的事情。我知道我上面提到的规则恰恰跟这条建议相反,我 也不喜欢这样搞。但是一些API确实使用了某些特殊返回值来表示方法中的异常,并且工作得很好,所以我还是觉得你们可以谨慎地遵循这条建议。

我看到了.NET Framework中很多获取资源的API方法使用了特殊返回值,比如Assembly.GetManifestStream方法,当找不到资源时(异常),它会返回null(不会抛出异常)。

12. 不要将“抛出异常”作为函数执行结果的一种

这是一个非常糟糕的设计。代码中包含太多的try/catch块会使代码难以理解,恰当的设计完全可以满足一个方法返回各种不同的执行结果(绝不可 能到了必须使用抛出异常的方式才能说明方法执行结果的地步,译者注),如果你确实需要通过抛出异常来表示方法的执行结果,那只能说明你这个方法做了太多事 情,必须进行拆分。

13. 可以使用“抛出异常”的方式去着重说明不能被忽略的错误

我可以举个现实中的例子。我为我的Grivo(我的一个产品)开发了一个用来登录的API(Login),如果用户登录失败,或者用户并没有调用 Login方法,那么他们调用其他方法时都会失败。我在设计Login方法的时候这样做的:如果用户登录失败,它会抛出一个异常,而并不是简单的返回 false。正因为这样,调用者(用户)才不会忽略(他还没登录)这个事实。

14.不要清空了堆栈跟踪(stack trace)信息

堆栈跟踪信息是异常发生时最重要的信息,我们经常需要在catch块中处理一些异常,有时候还需要重新上抛异常(re-throw)。下面来看看两种方法(一种错误的一种正确的):

错误的做法:

try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw ex;
}

为什么错了?因为当我们检查堆栈跟踪信息时,异常错误源变成了“thorw ex;”,这隐藏了真正异常抛出的位置。试一下下面这种做法:

try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw;
}

有什么变化没?我们使用“throw;”代替了“throw ex;”,后者会清空原来的堆栈跟踪信息。如果我们在抛出异常时没有指定具体的异常(简单的throw),那么它会默认地将原来捕获的异常继续上抛。这样 的话,上层代码捕获的异常还是最开始我们通过catch捕获的同一个异常。

15.异常类应标记为Serializable

很多时候,我们的异常需要能被序列化。当我们派生一个新的异常类型时,请不要忘了给它加上Serializable属性。谁会知道我们的异常类会不会用在Remoting Call或者Web Services中呢?

16.使用”抛出异常”代替Debug.Assert

当我们发布程序后,不要忘了Debug.Assert将会被忽略。我们在代码中做一些检查或者验证工作时,最好使用抛出异常的方式代替输出Debug信息。

将输出Debug信息这种方式用到单元测试或者那些只需要测试当软件真正发布后确保不会出错的场合。

17.不要重复造轮子

已经有很多在异常处理方面做得比较好的框架或库,微软提供的有两个:

Exception Management Application Block

Microsoft Enterprise Instrumentation Framework

注意,如果你不遵守我上面提到的一些规则,这些库对你来讲可能没什么用。

18.不要信任外部数据

外部数据是不可靠的,我们的软件程序在使用它们之前必须严格检查。无论这些外部数据来自于注册表、数据库、硬盘、socket还是你用键盘编写的文 件,所有这些外部数据在使用前必须严格进行检查。很多时候,我看到一些程序完全信任配置文件,因为开发这些程序的程序员总是认为没有人会编辑配置文件并损 坏它。

强类型检查和验证是避免bug发生的有力方法。你越早发现问题,就越早修复问题。几个月后再想搞清楚“为什么InvoiceItems表中的 ProductID栏会存在一个CustomerID数据?”是一件不太容易并且相当恼火的事情。如果你使用一个类代替基本类型(如int、 string)去存储客户(Customer)的数据的话,编译器就不会允许刚才那件事情(指将CustomerID和ProductID混淆)

推荐:《.NET/C#面试手册》

给.neter们整理了一份《.NET/C#面试手册》,目前大约4万字左右,初衷也很简单,就是希望在面试的时候能够帮助到大家,减轻大家的负担和节省时间。对于没有跳槽打算的也可以复习一下相关知识点,就当是查缺补漏!
都是一些经典的面试题目,目前主要分为10大部分。

  • .NET/C#面试手册:基础语法
  • .NET/C#面试手册:面向对象
  • .NET/C#面试手册:集合、异常、泛型、LINQ、委托、EF!
  • .NET/C#面试手册:多线程
  • .NET/C#面试手册:ASP.NET MVC
  • .NET/C#面试手册:ASP.NET Core
  • .NET/C#面试手册:ADO.NET、XML、HTTP、AJAX、WebService
  • .NET/C#面试手册:常见的算法
  • .NET/C#面试手册:数据库概念知识
  • .NET/C#面试手册:数据库SQL查询(附建表语句)
    废话不多说,本手册目前为第一版,后续慢慢也会陆续更新一些知识点,目前内容有以下板块:

《.NET/C#面试手册》包含[基础知识]、[面向对象]、[集合、异常、泛型、LINQ、委托、EF]、[ASP.NET MVC]、[ASP.NET Core]、[ADO.NET、XML、HTTP、AJAX、WebService]、[数据库知识]、[数据库SQL查询(附建表语句)]。

C#/.NET面试手册完整pdf、word获取

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C#异常处理18条最佳实践 的相关文章

  • 创建 DirectoryEntry 实例以供测试使用

    我正在尝试创建 DirectoryEntry 的实例 以便可以使用它来测试将传递 DirectoryEntry 的一些代码 然而 尽管进行了很多尝试 我还是找不到实例化 DE 并初始化它的 PropertyCollection 的方法 我有
  • 属性对象什么时候创建?

    由于属性实际上只是附加到程序集的元数据 这是否意味着属性对象仅根据请求创建 例如当您调用 GetCustomAttributes 时 或者它们是在创建对象时创建的 或者 前两个的组合 在由于 CLR 的属性扫描而创建对象时创建 从 CLR
  • Signalr 在生产服务器中总是陷入长轮询

    当我在服务器中托管应用程序时 它会检查服务器端事件并始终回退到长轮询 服务器托管环境为Windows Server 2012 R1和IIS 7 5 无论如何 我们是否可以解决这个问题 https cloud githubuserconten
  • Func 方法参数的首选命名约定是什么?

    我承认这个问题是主观的 但我对社区的观点感兴趣 我有一个缓存类 它采用类型的缓存加载器函数Func
  • 为什么 POSIX 允许在只读模式下超出现有文件结尾 (fseek) 进行搜索

    为什么寻找文件结尾很有用 为什么 POSIX 让我们像示例中那样在以只读方式打开的文件中进行查找 c http en cppreference com w c io fseek http en cppreference com w c io
  • 按字典顺序对整数数组进行排序 C++

    我想按字典顺序对一个大整数数组 例如 100 万个元素 进行排序 Example input 100 21 22 99 1 927 sorted 1 100 21 22 927 99 我用最简单的方法做到了 将所有数字转换为字符串 非常昂贵
  • .Net Core / 控制台应用程序 / 配置 / XML

    我第一次尝试使用新的 ConfigurationBuilder 和选项模式进入 Net Core 库 这里有很多很好的例子 https docs asp net en latest fundamentals configuration ht
  • A* 之间的差异 pA = 新 A;和 A* pA = 新 A();

    在 C 中 以下两个动态对象创建之间的确切区别是什么 A pA new A A pA new A 我做了一些测试 但似乎在这两种情况下 都调用了默认构造函数 并且仅调用了它 我正在寻找性能方面的任何差异 Thanks If A是 POD 类
  • 使用向量的 merge_sort 在少于 9 个输入的情况下效果很好

    不知何故 我使用向量实现了合并排序 问题是 它可以在少于 9 个输入的情况下正常工作 但在有 9 个或更多输入的情况下 它会执行一些我不明白的操作 如下所示 Input 5 4 3 2 1 6 5 4 3 2 1 9 8 7 6 5 4 3
  • 使用 LINQ 查找列表中特定类型的第一个元素

    使用 LINQ 和 C 在元素列表中查找特定类型的第一个项目的最短表示法是什么 var first yourCollection OfType
  • 我的 strlcpy 版本

    海湾合作委员会 4 4 4 c89 我的程序做了很多字符串处理 我不想使用 strncpy 因为它不会终止 我不能使用 strlcpy 因为它不可移植 只是几个问题 我怎样才能让我的函数正常运行 以确保它完全安全稳定 单元测试 这对于生产来
  • Windows 10 中 Qt 桌面应用程序的缩放不当

    我正在为 Windows 10 编写一个简单的 Qt Widgets Gui 应用程序 我使用的是 Qt 5 6 0 beta 版本 我遇到的问题是它根本无法缩放到我的 Surfacebook 的屏幕上 这有点难以判断 因为 SO 缩放了图
  • 可空属性与可空局部变量

    我对以下行为感到困惑Nullable types class TestClass public int value 0 TestClass test new TestClass Now Nullable GetUnderlyingType
  • AccessViolationException 未处理

    我正在尝试使用史蒂夫 桑德森的博客文章 http blog stevensanderson com 2010 01 28 editing a variable length list aspnet mvc 2 style 为了在我的 ASP
  • 将日期参数传递给对 MVC 操作的 ajax 调用的安全方法

    我有一个 MVC 操作 它的参数之一是DateTime如果我通过 17 07 2012 它会抛出一个异常 指出参数为空但不能有空值 但如果我通过01 07 2012它被解析为Jan 07 2012 我将日期传递给 ajax 调用DD MM
  • 作为字符串的动态属性名称

    使用 DocumentDB 创建新文档时 我想设置属性名称动态地 目前我设置SomeProperty 像这样 await client CreateDocumentAsync dbs db colls x new SomeProperty
  • 如何构建印度尼西亚电话号码正则表达式

    这些是一些印度尼西亚的电话号码 08xxxxxxxxx 至少包含 11 个字符长度 08xxxxxxxxxxx 始终以 08 开头 我发现这个很有用 Regex regex new Regex 08 0 9 0 9 0 9 0 9 0 9
  • GDK3/GTK3窗口更新的精确定时

    我有一个使用 GTK 用 C 语言编写的应用程序 尽管该语言对于这个问题可能并不重要 这个应用程序有全屏gtk window与单个gtk drawing area 对于绘图区域 我已经通过注册了一个刻度回调gtk widget add ti
  • 如何将字符串“07:35”(HH:MM) 转换为 TimeSpan

    我想知道是否有办法将 24 小时时间格式的字符串转换为 TimeSpan 现在我有一种 旧时尚风格 string stringTime 07 35 string values stringTime Split TimeSpan ts new
  • 不同类型的指针可以互相分配吗?

    考虑到 T1 p1 T2 p2 我们可以将 p1 分配给 p2 或反之亦然吗 如果是这样 是否可以不使用强制转换来完成 或者我们必须使用强制转换 首先 让我们考虑不进行强制转换的分配 C 2018 6 5 16 1 1 列出了简单赋值的约束

随机推荐

  • GPU指令下发方式

    1 概述 GPU接收CPU发送的渲染命令 执行相应的计算 渲染命令在CPU和GPU之间传递 由CPU发送给GPU AMD的GPU有两种命令发送方式 第一种是CPU通过直接写GPU的寄存器 发送相应的渲染命令 对于GPU来说 这种方式是CPU
  • Vue中table实现行的上移和下移

    链接 html
  • 【基础知识学习】链表的创建

    链表的创建 因为对链表使用不太熟悉 学习使用数组创建了一个链表并访问 代码如下 以后学到新的知识继续补充 include
  • mfc入门基础(七)向导对话框的创建与显示

    实现参考 VS2010 MFC编程入门之十四 对话框 向导对话框的创建及显示 软件开发 鸡啄米 一 向导对话框的创建与显示 1 具体的例子使用 还是参照上节或者说上上节中的例子写出来 test02 所以也还是在这个基础上来进行更改 2 创建
  • Day82-基于ElasticSearch的实战-仿京东搜素

    基于ElasticSearch的实战 仿京东搜素 1 创建springboot项目 添加相关依赖 2 导入相关Maven依赖
  • HAL库使用硬件SPI驱动0.96寸OLED stm32F401

    找一个可以使用SPI接口的OLED驱动程序 一般买OLED会提供 或者自己网上找 这里用的是中景园的例程 由于我使用的开发板是STM32F401ccu6 所以我先移植到我的开发板上 主要改的 打开MXcube 配置时钟 配置DEBUG 我这
  • RBM中的Gibbs,CD-K,PCD三种抽样方式

    首先看RBM教程推导 http blog csdn net itplus article details 19207371 推导到下图时 对中括号中的第二项进行计算 是 通过采样的到的 那么采样有三种方法 Gibbs CD K PCD 下面
  • JDBC的原理及应用

    一 JDBC作用 JDBC Java DataBase Connectivity 就是Java数据库连接 简单说就是封装了java语言操作不同据库 例如 mysql oracle sqlServer等数据库 的接口 各个数据库厂商实现这个接
  • SpringBoot 多模块的优点与必要性

    使用多模块的原因 使用Java技术开发的工程项目 无论是数据处理系统还是Web网站 随着项目的不断发展 需求的不断细化与添加 工程项目中的代码越来越多 包结构也越来越复杂这时候工程的进展就会遇到各种问题 1 不同方面的代码之间相互耦合 这时
  • vsCode注释快捷键

    系列文章目录 文章目录 系列文章目录 前言 一 pandas是什么 二 使用步骤 1 你也可以选择代码 使用上方菜单的 编辑 来手动注释代码 前言 VScode怎么注释掉一段代码 快捷键直接搞定 一 pandas是什么 示例 pandas
  • vue 封装组件供全局使用_Vue如何封装高质量组件

    依照MVC模式思想来解释高可复用性 低耦合性的组件定义方法 组件分类 级别从小到大 基础组件 只是一种宽泛的定义 可见场景较多 与业务无关的组件 比如列表 表格 输入框等 没有实际的UI 故不能直接使用 UI组件 与基础组件同级 为基础组件
  • python os.walk 指定遍历深度_使用os.walk()以递归方式遍历Python中的目录

    试试这个 usr bin env python coding utf 8 FileTreeMaker py author legendmohe import os import argparse import time class File
  • STM32实现74HC595控制

    一 前言 本文主要是实出74HC595的简单控制功能实现 学习笔记整理 二 概述 一 74HC595简述 74HC595是一个8位串行输入 平行输出的位移缓存器 平台行输出为三态输出 在SCHCP的上升沿输入 在STCP的上升沿进入存储寄存
  • 测试开发-晋级之路4-函数进阶(闭包 装饰器)***

    一 闭包 在上面我们见过了再函数中调用函数本身 那么在函数中可不可以定义一个函数 问题需求 如何函故外部调用函数内部定义的函数 问题的引入 到底什么是闭包 闭包的概念 一个完整的闭包须满足一下三个条件 1 函数中嵌套一个函数 2 外层函数返
  • ubuntu系统下配置vscode编译cmake

    文章目录 一 配置vs code运行c 代码 三个关键文件介绍 1 tasks json run helloworld cpp 1 1 打开cpp文件 使其成为活动文件 1 2 按编辑器右上角的播放按钮 1 3生成task文件 1 4 此时
  • 如何在工作中利用AIGC提质增效?

    引言 人工智能技术快速发展 以 ChatGPT 为代表的新的人工智能语言模型的出现与更迭 引发人们极大的兴奋和关注 越来越多的企业开始将 AI 技术应用到生产流程 以提高工作效率和生产力 AIGC AI Generated Content
  • SQLMAP 脱库过程(post请求,三种方法)

    一 准备工作 1 sqlmap为python语言开发 因此需要具备python环境 2 python环境搭建及sqlmap下载 可参考其他文章 很多也很全 这个不是本文重点 因此略过 二 环境搭建 1 本人使用的是Mac windows7虚
  • Python自动化运维之一(Python入门)

    Python简介 python是吉多 范罗苏姆发明的一种面向对象的脚本语言 可能有些人不知道面向对象和脚本具体是什么意思 但是对于一个初学者来说 现在并不需要明白 大家都知道 当下全栈工程师的概念很火 而Python是一种全栈的开发语言 所
  • AdaDelta算法

    记录一下自己的学习过程 也能让自己的印象更深吧 AdaDelta算法主要是为了解决AdaGrad算法中存在的缺陷 下面先介绍一下AdaGrad算法优点和以及存在的问题 AdaGrad的迭代公式如下所示 x t
  • C#异常处理18条最佳实践

    首先 异常处理应该是系统设计规约的一部分出现在系统设计文档中 而不仅仅是一种技术实现 作为设计文档的一部分 异常处理应该着眼于系统容错性和稳定性 正如楼主提到的那样 然后在根据这个规约 再来具体讨论和选择异常处理中使用的各种技术细则 比如