前言  

  自从.NET出现后,关于CLR异常机制的讨论就几乎从未停止过。迄今为止,CLR异常机制让人关注最多的一点就是“效率”问题。其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题,只有引发异常时才会带来效率问题。基于这一点,很多开发者已经达成共识:不应将异常机制用于正常控制流中。达成的另一个共识是:CLR异常机制带来的“效率”问题不足以“抵消”它带来的巨大收益。CLR异常机制至少有一下几个优点:

  1、正常控制流会倍立即中止,无效值或状态不会在系统中继续传播。

  2、提供了统一处理错误的方法。

  3、提供了在构造函数、操作符重载及属性中报告异常的便利机制。

  4、提供了异常堆栈,便于开发者定位异常发生的位置。

  另外,“异常”其名称本身就说明了它的发生是一个小概率事件。所以,因异常带来的效率问题会倍限制在一个很小的范围内。实际上,try catch所带来的效率问题几乎忽略的。在某些特定的场合,如Int32的Parse方法中, 确实存在这因为滥用而导致的效率问题。在这种情况下,我们就应该考虑提供一个TryParse方法,从设计的角度让用户选择让程序运行得更快。另一种规避因为异常而影响效率的方法是:Tester-doer模式,下文将详细阐述。

  本章将给出一些在C#中处理CLR异常方面的通用建议,一帮助大家构建和开发一个运行良好和可靠的应用系统。

  本文已同步到http://www.cnblogs.com/aehyok/p/3624579.html。本文主要来学习以下几点建议

  建议58、用抛出异常代替返回错误代码

  建议59、不要在不恰当的场合下引发异常

  建议60、重新引发异常时使用inner Exception

58、用抛出异常代替返回错误代码  

  在异常机制出现之前,应用程序普遍采用返回错误代码的方式来通知调用者发生了异常。本建议首先阐述为什么要用抛出异常的方式来代替返回错误代码的方式。

  对于一个成员方法来说,它要么执行成功,要么执行失败。成员方法成功的情况很容易理解。但是如果执行失败了却没有那么简单,因为我们需要将导致执行失败的原因通知调用者。抛出异常和返回错误代码都是用来通知调用者的手段。

  假设我们要实现这样一个简单的功能:应用程序需要完成一次保存新建用户的操作。这是一个分布式的操作,保存动作除了需要将用户保存在本地外,还需要通过WCF在远程服务器上保存数据。负责保存用户的成员方法如下:

  1. public int SaveUser(User user)
  2. {
  3. if (!SaveToFile(user))
  4. {
  5. return ;
  6. }
  7. if (!SaveToDataBase(user))
  8. {
  9. return ;
  10. }
  11. return ;
  12. }
  13.  
  14. public bool SaveToFile(User user)
  15. {
  16. return true;
  17. }
  18.  
  19. public bool SaveToDataBase(User user)
  20. {
  21. return true;
  22. }

如果单纯的看SaveUser方法,似乎一切都还不错,在约定好了错误代码后,调用者只要接收到1或2,就知道到底是那里出现了问题。但仔细研究会发现,如果方法执行失败,似乎还可以挖掘出更多的原因。

假设在SaveToFile方法中,我们可能会遇到:

1、程序无数据存储文件写权限导致的失败。

2、硬盘空间不足导致的失败。

在SaveToDataBase方法中,我们可能会遇到:

1、服务不存在导致的失败。

2、网络连接不正常导致的失败。

当我们想要告诉调用者更多的细节的时候,就需要与调用者约定更多的错误代码。于是我们很快就会发现,错误代码飞速膨胀,直到看起来似乎无法维护。因为我们总在查找并确认错误代码。

  采用接下来的方法,可能会省略很大一部分的错误代码:

  1. public bool SaveUser1(User user,ref string errorMessage)
  2. {
  3. if (!SaveToFile(user))
  4. {
  5. errorMessage = "本地保存失败";
  6. return false;
  7. }
  8. if (!SaveToDataBase(user))
  9. {
  10. errorMessage = "远程保存失败";
  11. return false;
  12. }
  13. return true;
  14. }

  这看上去不错,即使存在更多的错误也可以将失败信息呈现给调用者或者上层用户。然后仅仅呈现失败信息就可以了吗?我们来看看这样一种情况:给失败通知增加稍微复杂一点的功能。

  如果本地保存失败,要完成“通知运行本段代码的客户机管理员”的功能。通常情况下,仅仅只需要显示类似的信息:“本地保存失败,请检查用户权限”。如果远程保存失败,应用程序需要“发送一封邮件给远程服务器的系统管理员”。总金额个增加的功能导致我们不能像处理“本地保存失败”那样来处理“远程保存失败”。

  一切仿佛又回到了起点,在没有异常处理机制之前,我们只能返回错误代码,但是现在有了另一种选择,即使用异常机制。如果使用异常机制,那么最终的代码看起来应该是下面这样的:

  1. static void Main(string[] args)
  2. {
  3. try
  4. {
  5. SaveUser(new User());
  6. }
  7. catch (IOException e)
  8. {
  9. ///IO异常,通知当前用户
  10. }
  11. catch (UnauthorizedAccessException e)
  12. {
  13. ////权限异常,通知客户端管理员
  14. }
  15. catch (CommunicationException e)
  16. {
  17. ///网络异常,通知发送给网络管理员
  18. }
  19. }
  20.  
  21. public static void SaveUser(User user)
  22. {
  23. SaveToFile(user);
  24.  
  25. SaveToDataBase(user);
  26. }

  使用CLR异常机制后,我们会发现代码变得更清晰、更易于理解了。至于效率问题,还可以重新审视“效率”的立足点:throw exception产生的那点效率损耗与等待网络连接异常相比,简直微不足道,而CLR异常机制带来的好处却是显而易见的。

  这里需要稍加强调的是,在catch(CommunicationException)这个代码块中,代码所完成的功能是“通知发送”而不是“发送”本身,因为我们要确保在catch和finally中所执行的代码是可以倍执行的。换句话说,尽量不要在catch和finally中再让代码“出错”,那么让异常堆栈信息变得复杂和难以理解。

  在本例的catch代码块中,不要真得编写发送邮件的代码,因为发送邮件这个行为可能会产生更多的异常,而“通知发送”这个行为稳定性更高(即不“出错”)。

  以上通过实际的案例阐述了抛出异常相比于返回错误代码的优越性,以及在某些情况下错误代码将无用武之地,如构造函数、操作符重载及属性。语法特性决定了其不能具备任何返回值,于是异常机制倍当作取代错误代码的首要选择。

59、不要在不恰当的场合下引发异常  

  最常见不易引发异常的情况是对在可控范围内的输入和输出引发异常。如下面的代码所示:

  1. public void SaveUser2(User user)
  2. {
  3. if (user.Age < )
  4. {
  5. throw new ArgumentOutOfRangeException("Age不能为负数");
  6. }
  7. }

暂时可以发现此方法有两处不妥:

1、判断Age为负数。这是一个正常的业务逻辑,它不应该倍处理为一个异常。

2、应该采用Tester-Doer来验证输入。

我们现在来添加一个Tester方法

  1. public static bool CheckAge(int age,ref string errorMessage)
  2. {
  3. if (age < )
  4. {
  5. errorMessage = "Age不能为负数";
  6. return false;
  7. }
  8. else if (age > )
  9. {
  10. errorMessage = "Age不能大于100";
  11. return false;
  12. }
  13. return true;
  14. }

而调用的地方看起来是这样的

  1. string errorMessage = string.Empty;
  2. if (CheckAge(, ref errorMessage))
  3. {
  4. SaveUser(new User());
  5. }

程序员,尤其是类库开发程序员,要掌握的两条首要原则是:

正常的业务流程不应使用异常来处理。

不要总是尝试去捕获异常或引发异常,而应该允许异常向调用堆栈往上传播。

那么到底应该在什么情况下引发异常呢?

第一种情况 如果运行代码后会造成内存泄漏、资源不可用,或者应用程序状态不可恢复,则引发异常。

第二种情况 在捕获异常的时候,如果需要包装一些更有用的信息, 则引发异常。

这类异常的引发在UI层特别有用。系统引发的异常所带的信息往往更倾向于技术性的描述;而在UI层,面对异常的很可能是最终的用户。如果需要将异常信息呈现给用户,更好的做法是先包装异常,然后引发一个包含友好信息的新异常。

第三种情况 如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常。

例如下面的代码中:

  1. public void CaseSample(object o)
  2. {
  3. if (o == null)
  4. {
  5. throw new ArgumentNullException("o");
  6. }
  7. User user = null;
  8. try
  9. {
  10. user = (User)o;
  11. }
  12. catch (InvalidCastException)
  13. {
  14. throw new ArgumentException("输入参数不是一个User", "o");
  15. }
  16. }

如果抛出InvalidCastException则没有任何意义,甚至会造成误解,所以更好的方式是抛出一个ArgumentException。

需要重点介绍的正确引发异常的典型例子就是捕获底层API错误代码,并抛出。查看如下代码:

  1. public void Test()
  2. {
  3. int errorCode=Marshal.GetLastWin32Error();
  4. if (errorCode == )
  5. {
  6. throw new InvalidOperationException("具体错误");
  7. }
  8. }

很显然当需要调用WIndows API或第三方API提供的接口时,如果对方的异常报告机制使用的是错误代码,最好重新引发该接口提供的错误,因为你需要让自己的团队更好地理解这些错误。

建议60、重新引发异常时使用inner Exception  

  当捕获了某个异常,将其包装或重新引发异常的时候,如果其中包含了Inner Exception,则有助于程序员分析内部信息,方便调试。

  可以先来查看以下代码

  1. static void Main(string[] args)
  2. {
  3. try
  4. {
  5. Test();
  6. }
  7. catch (Exception err)
  8. {
  9. Console.WriteLine(err.Message);
  10. if (err.InnerException != null)
  11. {
  12. Console.WriteLine(err.InnerException.Message);
  13. }
  14. }
  15. }
  16.  
  17. public static void Test()
  18. {
  19. try
  20. {
  21. SaveUser(new User());
  22. }
  23. catch (Exception err)
  24. {
  25. var ex = new Exception("网络链接失败,请稍后再试",err);
  26. //throw err; //这样抛出异常会丢掉异常原有的堆栈信息
  27. throw ex;
  28. }
  29. }

如果不想使用Inner Exception,可以使用如下方式

  1. static void Main(string[] args)
  2. {
  3. try
  4. {
  5. Test();
  6. }
  7. catch (Exception err)
  8. {
  9. Console.WriteLine(err.Data["SockInfo"].ToString());
  10. }
  11. }
  12.  
  13. public static void Test()
  14. {
  15. try
  16. {
  17. SaveUser(new User());
  18. }
  19. catch (Exception err)
  20. {
  21. err.Data.Add("SockInfo", "网络链接失败,请稍后再试");
  22. throw err;
  23. }
  24. }

相当于把Test方法中的异常当作Inner  Exception,然后向上抛出。

意思其实也就是将异常进行简单的封装,然后继续向上抛出,让上层来捕获异常信息。

英语小贴士

1、I see. ——我明白了。

2、 I quit! ——我不干了!

3. Let go! ——放手!

4. Me too. ——我也是。

5. My god! ——天哪!

6. No way! ——不行!

7. Come on. ——来吧(赶快)

8. Hold on. ——等一等。

9. I agree。 ——我同意。

10. Not bad. ——还不错。

作者:aehyok

出处:http://www.cnblogs.com/aehyok/

感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。

编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误、不要在不恰当的场合下引发异常、重新引发异常时使用inner Exception]的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

随机推荐

  1. 关于点击空白关闭弹窗的js写法推荐

    $(document).mouseup(function(e){ var _con = $(' 目标区域 '); // 设置目标区域 if(!_con.is(e.target) && ...

  2. 手机打开PC端网址自动跳转到手机站代码

    <script>function uaredirect(murl){ try { if(document.getElementById("bdmark") != nul ...

  3. 【C++】array初始化0

    让代码...优雅? ==================分割线==================== 局部数组:没有默认值,如果声明的时候不定义,则会出现随机数(undefined):如果声明的长度 ...

  4. poj1274 The Perfect Stall (二分最大匹配)

    Description Farmer John completed his new barn just last week, complete with all the latest milking ...

  5. C/C++学习----使用C语言代替cmd命令、cmd命令大全

    [开发环境] 物理机版本:Win 7 旗舰版(64位) IDE版本:Visual Studio 2013简体中文旗舰版(cn_visual_studio_ultimate_2013_with_upda ...

  6. AC日记——逃出克隆岛 (bfs)

    2059 逃出克隆岛  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题解       题目描述 Description oi小组的yh酷爱玩魔兽rpg,每天都 ...

  7. java 21-11 数据输入、输出流和内存操作流

    IO数据流: 可以读写基本数据类型的数据 数据输入流:DataInputStream DataInputStream(InputStream in)   数据输出流:DataOutputStream ...

  8. Nginx反向代理+负载均衡简单实现(http方式)

    1)nginx的反向代理:proxy_pass2)nginx的负载均衡:upstream 下面是nginx的反向代理和负载均衡的实例: 负载机:A机器:103.110.186.8/192.168.1. ...

  9. PHP mcrypt加密扩展使用总结

    在开发中,很多时候我们在前后端交互中需要对一些敏感数据进行一定的加密.PHP中有提供了mcrypt的这样一个加密扩展实现对数据的加密解密. 一.mcrypt扩展的安装 在低版本的PHP中需要在配置文件 ...

  10. 发发关于JavaScript的感慨,随手记几个js知识碎片

    最近一段时间写了很多JavaScript和jquery代码,越来越感觉js基础不牢固,写一句查半天,有时间肯定要系统的学一下. 不说了,先记一下最近学到的点东西,省的以后没时间系统学js还要再来查. ...