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

dotNet开发汇总 专栏收录该内容
31 篇文章 1 订阅

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

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

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

软件中有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获取

  • 0
    点赞
  • 1
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
《.NET最佳实践》是.NET领域的集大成之作和公认经典。书中总结了大量.NET开发人员公认的最佳实践,并有针对性地阐明了如何恰当地在个人、小组、组织和行业中应用这些最佳实践。书中的最佳实践涵盖.NET开发的方方面面,从基础技术到核心技术,从开发方法到开发流程,从开发工具到团队协作,等等。所有最佳实践都辅之以实际的项目案例,并对案例代码进行了深入分析,实践性和可操作性极强。 全书可分为四个部分。第一部分(第1~5章)着重教授你如何选择和发现这些实践,如何提升团结度,如何量化这些实践等。第二部分(第6~9章)专注于开发人员个人的实践。第6章涵盖.NET规章制度标准和指南,识别并避免“代码异味”的技巧。第7章以非常规的方式来重新审视C#语言,带给你全新体验。第8章分享了多个符合高效开发原则的测试代码改进方法。第9章介绍生成自动化,以消除易错步骤,便于生成和部署。第三部分(第10~12章)重点介绍支持的工具、产品和技术。第10章介绍持续集成的流程和步骤,同时分析持续集成可利用的产品和工具。第11章推荐并分析诸多实用的代码分析(静态和动态)的工具、技术和方法。第12章综合介绍多种商业的及开源的测试框架和工具。第四部分(第13章)告诉你如何说服对采用最佳实践存在反感和偏见的组织和个人。本书还附有评估、跟踪最佳实践的记分卡。 《.NET最佳实践》 译者序 前 言 第1章 冷静待之 1 1.1 实践选择 3 1.1.1 可行性 3 1.1.2 认可度 5 1.1.3 价值 6 1.1.4 原型 6 1.2 关注需要改善的目标领域 7 1.2.1 产品交付 8 1.2.2 软件质量 9 1.2.3 团队关系 10 1.3 整体改善 11 1.3.1 均衡 11 1.3.2 面貌一新 12 1.3.3 可持续性 12 1.4 小结 13 第2章 .NET实践领域 15 2.1 从内部挖掘 17 2.1.1 技术债 17 2.1.2 缺陷跟踪系统 18 2.1.3 反思分析 19 2.1.4 前瞻性分析 20 2.2 应用程序生命周期管理 20 2.3 设计模式和开发指南 22 2.3.1 .NET设计规范 23 2.3.2 微软的模式和实践小组 23 2.3.3 显示界面层设计模式 24 2.3.4 对象–对象映射 25 2.3.5 依赖注入 25 2.4 研究和开发 26 2.4.1 自动化测试生成工具 27 2.4.2 契约式编码 30 2.5 微软安全开发生命周期 30 2.6 小结 32 第3章 实现预期目标 33 3.1 成功要素 34 3.1.1 项目启动阶段 37 3.1.2 超出范围 38 3.1.3 干扰和分心 38 3.1.4 学习与工作之间的平衡 39 3.2 共识 39 3.2.1 线框图 40 3.2.2 文档化架构 41 3.2.3 报表模型 42 3.2.4 细节化示例 42 3.2.5 创建原型 43 3.3 预期目标 44 3.3.1 交付 44 3.3.2 实际成果 44 3.3.3 趋势 46 3.4 小结 47 第4章 量化价值 48 4.1 价值 50 4.1.1 财务回报 50 4.1.2 提高可控性 51 4.1.3 提高质量品质 52 4.1.4 更加高效 53 4.2 数据来源 54 4.2.1 定量数据 54 4.2.2 定性数据 57 4.2.3 业界证据 58 4.3 小结 59 第5章 战略 60 5.1 认知 62 5.1.1 头脑风暴 62 5.1.2 规划 63 5.1.3 监控 64 5.1.4 沟通 64 5.2 个体过程 66 5.2.1 卓越的承诺 67 5.2.2 良性的纪律 67 5.2.3 效力和坚持 68 5.3 杠杆 69 5.3.1 自动化 69 5.3.2 警报系统 70 5.3.3 经验和专业知识 71 5.4 小结 71 第6章 .NET的规章制度 72 6.1 编码标准和指南 73 6.1.1 来源 73 6.1.2 异常 75 6.1.3 disposable模式 78 6.1.4 其他事项 82 6.2 代码异味 86 6.2.1 注释 86 6.2.2 过于复杂 88 6.2.3 未用到的、无法到达的和无效的代码 89 6.3 小结 89 第7章 强大的C#构造 90 7.1 扩展方法 91 7.2 隐式类型的局部变量 94 7.3 可以为null的类型 96 7.4 null合并运算符 98 7.5 可选参数 99 7.6 泛型 102 7.7 LINQ 103 7.8 小结 108 第8章 自动测试 109 8.1 案例研究 111 8.2 棕色地带的应用程序 112 8.3 绿色地带的应用程序 114 8.4 自动测试基础 115 8.5 测试代码的可维护性 115 8.5.1 命名约定 116 8.5.2 测试方法主体 119 8.6 单元测试 131 8.6.1 边界分析 133 8.6.2 无效参数 134 8.6.3 无效的前提件 136 8.7 伪造、存根和模拟 137 8.7.1 隔离被测试代码 137 8.7.2 测试依赖交互 139 8.8 表面测试 140 8.9 自动集成测试 143 8.10 数据库注意事项 144 8.11 小结 145 第9章 生成自动化 146 9.1 生成工具 147 9.2 MSBuild基础 148 9.2.1 任务和目标 148 9.2.2 PropertyGroup和ItemGroup 150 9.2.3 基本任务 154 9.3 日志 155 9.4 参数和变量 157 9.5 库和扩展 159 9.6 导入和包含 160 9.7 内联任务 161 9.8 常见任务 163 9.8.1 日期和时间 163 9.8.2 程序集信息 164 9.8.3 XML的Peek和Poke 165 9.8.4 zip存档 166 9.9 自动部署 167 9.9.1 生成一次,部署多次 168 9.9.2 打包工具 169 9.9.3 部署工具 169 9.10 小结 170 第10章 持续集成 171 10.1 案例研究 172 10.2 CI服务器 173 10.2.1 CruiseControl.NET 174 10.2.2 Jenkins 175 10.2.3 TeamCity 175 10.2.4 Team Foundation Server 176 10.3 CI生命周期 176 10.3.1 重新生成 177 10.3.2 单元测试 182 10.3.3 分析 184 10.3.4 打包 187 10.3.5 部署 188 10.3.6 稳定性测试 190 10.3.7 生成报告 192 10.4 小结 192 第11章 代码分析 193 11.1 案例研究 195 11.2 静态分析 196 11.2.1 程序集分析 197 11.2.2 源代码分析 205 11.2.3 架构和设计 211 11.2.4 代码度量值 213 11.2.5 质量保证指标 213 11.3 动态分析 214 11.3.1 代码覆盖率 214 11.3.2 性能分析 216 11.3.3 查询分析 217 11.3.4 日志 217 11.4 小结 218 第12章 测试框架 219 12.1 单元测试框架 220 12.2 测试运行器 221 12.2.1 NUnit的GUI和控制台运行器 221 12.2.2 ReSharper测试运行器 224 12.2.3 Visual Studio测试运行器 227 12.2.4 Gallio测试运行器 230 12.2.5 xUnit.net测试运行器 231 12.3 xUnit测试模式 233 12.3.1 标识测试方法 233 12.3.2 标识测试类和夹具 234 12.3.3 断言 239 12.4 模拟对象框架 240 12.4.1 使用Rhino Mocks动态生成伪对象 240 12.4.2 使用Moles进行隔离测试 243 12.5 数据库测试框架 248 12.6 用户界面测试框架 252 12.6.1 Web应用程序测试框架 252 12.6.2 Windows窗体和其他用户界面测试框架 253 12.7 验收测试框架 253 12.7.1 测试规范和行为 254 12.7.2 业务逻辑验收测试 255 12.8 小结 256 第13章 反感和偏见 257 13.1 团体利益偏见 258 13.2 玫瑰色回顾 259 13.3 团体与个人的评价 260 13.4 维持现状和辩解机制 260 13.5 优势错觉 261 13.6 达克效应 261 13.7 鸵鸟效应 262 13.8 赌徒谬误 263 13.9 歧义效应 263 13.10 集中效应 264 13.11 双曲贴现 264 13.12 常态偏见 265 13.13 小结 265 附录A 参考资源 266 附录B 记分卡 279
相关推荐
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页

打赏

dotnet全栈开发

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值