编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误、不要在不恰当的场合下引发异常、重新引发异常时使用inner Exception]
前言
自从.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在远程服务器上保存数据。负责保存用户的成员方法如下:
public int SaveUser(User user)
{
if (!SaveToFile(user))
{
return ;
}
if (!SaveToDataBase(user))
{
return ;
}
return ;
} public bool SaveToFile(User user)
{
return true;
} public bool SaveToDataBase(User user)
{
return true;
}
如果单纯的看SaveUser方法,似乎一切都还不错,在约定好了错误代码后,调用者只要接收到1或2,就知道到底是那里出现了问题。但仔细研究会发现,如果方法执行失败,似乎还可以挖掘出更多的原因。
假设在SaveToFile方法中,我们可能会遇到:
1、程序无数据存储文件写权限导致的失败。
2、硬盘空间不足导致的失败。
在SaveToDataBase方法中,我们可能会遇到:
1、服务不存在导致的失败。
2、网络连接不正常导致的失败。
当我们想要告诉调用者更多的细节的时候,就需要与调用者约定更多的错误代码。于是我们很快就会发现,错误代码飞速膨胀,直到看起来似乎无法维护。因为我们总在查找并确认错误代码。
采用接下来的方法,可能会省略很大一部分的错误代码:
public bool SaveUser1(User user,ref string errorMessage)
{
if (!SaveToFile(user))
{
errorMessage = "本地保存失败";
return false;
}
if (!SaveToDataBase(user))
{
errorMessage = "远程保存失败";
return false;
}
return true;
}
这看上去不错,即使存在更多的错误也可以将失败信息呈现给调用者或者上层用户。然后仅仅呈现失败信息就可以了吗?我们来看看这样一种情况:给失败通知增加稍微复杂一点的功能。
如果本地保存失败,要完成“通知运行本段代码的客户机管理员”的功能。通常情况下,仅仅只需要显示类似的信息:“本地保存失败,请检查用户权限”。如果远程保存失败,应用程序需要“发送一封邮件给远程服务器的系统管理员”。总金额个增加的功能导致我们不能像处理“本地保存失败”那样来处理“远程保存失败”。
一切仿佛又回到了起点,在没有异常处理机制之前,我们只能返回错误代码,但是现在有了另一种选择,即使用异常机制。如果使用异常机制,那么最终的代码看起来应该是下面这样的:
static void Main(string[] args)
{
try
{
SaveUser(new User());
}
catch (IOException e)
{
///IO异常,通知当前用户
}
catch (UnauthorizedAccessException e)
{
////权限异常,通知客户端管理员
}
catch (CommunicationException e)
{
///网络异常,通知发送给网络管理员
}
} public static void SaveUser(User user)
{
SaveToFile(user); SaveToDataBase(user);
}
使用CLR异常机制后,我们会发现代码变得更清晰、更易于理解了。至于效率问题,还可以重新审视“效率”的立足点:throw exception产生的那点效率损耗与等待网络连接异常相比,简直微不足道,而CLR异常机制带来的好处却是显而易见的。
这里需要稍加强调的是,在catch(CommunicationException)这个代码块中,代码所完成的功能是“通知发送”而不是“发送”本身,因为我们要确保在catch和finally中所执行的代码是可以倍执行的。换句话说,尽量不要在catch和finally中再让代码“出错”,那么让异常堆栈信息变得复杂和难以理解。
在本例的catch代码块中,不要真得编写发送邮件的代码,因为发送邮件这个行为可能会产生更多的异常,而“通知发送”这个行为稳定性更高(即不“出错”)。
以上通过实际的案例阐述了抛出异常相比于返回错误代码的优越性,以及在某些情况下错误代码将无用武之地,如构造函数、操作符重载及属性。语法特性决定了其不能具备任何返回值,于是异常机制倍当作取代错误代码的首要选择。
59、不要在不恰当的场合下引发异常
最常见不易引发异常的情况是对在可控范围内的输入和输出引发异常。如下面的代码所示:
public void SaveUser2(User user)
{
if (user.Age < )
{
throw new ArgumentOutOfRangeException("Age不能为负数");
}
}
暂时可以发现此方法有两处不妥:
1、判断Age为负数。这是一个正常的业务逻辑,它不应该倍处理为一个异常。
2、应该采用Tester-Doer来验证输入。
我们现在来添加一个Tester方法
public static bool CheckAge(int age,ref string errorMessage)
{
if (age < )
{
errorMessage = "Age不能为负数";
return false;
}
else if (age > )
{
errorMessage = "Age不能大于100";
return false;
}
return true;
}
而调用的地方看起来是这样的
string errorMessage = string.Empty;
if (CheckAge(, ref errorMessage))
{
SaveUser(new User());
}
程序员,尤其是类库开发程序员,要掌握的两条首要原则是:
正常的业务流程不应使用异常来处理。
不要总是尝试去捕获异常或引发异常,而应该允许异常向调用堆栈往上传播。
那么到底应该在什么情况下引发异常呢?
第一种情况 如果运行代码后会造成内存泄漏、资源不可用,或者应用程序状态不可恢复,则引发异常。
第二种情况 在捕获异常的时候,如果需要包装一些更有用的信息, 则引发异常。
这类异常的引发在UI层特别有用。系统引发的异常所带的信息往往更倾向于技术性的描述;而在UI层,面对异常的很可能是最终的用户。如果需要将异常信息呈现给用户,更好的做法是先包装异常,然后引发一个包含友好信息的新异常。
第三种情况 如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常。
例如下面的代码中:
public void CaseSample(object o)
{
if (o == null)
{
throw new ArgumentNullException("o");
}
User user = null;
try
{
user = (User)o;
}
catch (InvalidCastException)
{
throw new ArgumentException("输入参数不是一个User", "o");
}
}
如果抛出InvalidCastException则没有任何意义,甚至会造成误解,所以更好的方式是抛出一个ArgumentException。
需要重点介绍的正确引发异常的典型例子就是捕获底层API错误代码,并抛出。查看如下代码:
public void Test()
{
int errorCode=Marshal.GetLastWin32Error();
if (errorCode == )
{
throw new InvalidOperationException("具体错误");
}
}
很显然当需要调用WIndows API或第三方API提供的接口时,如果对方的异常报告机制使用的是错误代码,最好重新引发该接口提供的错误,因为你需要让自己的团队更好地理解这些错误。
建议60、重新引发异常时使用inner Exception
当捕获了某个异常,将其包装或重新引发异常的时候,如果其中包含了Inner Exception,则有助于程序员分析内部信息,方便调试。
可以先来查看以下代码
static void Main(string[] args)
{
try
{
Test();
}
catch (Exception err)
{
Console.WriteLine(err.Message);
if (err.InnerException != null)
{
Console.WriteLine(err.InnerException.Message);
}
}
} public static void Test()
{
try
{
SaveUser(new User());
}
catch (Exception err)
{
var ex = new Exception("网络链接失败,请稍后再试",err);
//throw err; //这样抛出异常会丢掉异常原有的堆栈信息
throw ex;
}
}
如果不想使用Inner Exception,可以使用如下方式
static void Main(string[] args)
{
try
{
Test();
}
catch (Exception err)
{
Console.WriteLine(err.Data["SockInfo"].ToString());
}
} public static void Test()
{
try
{
SaveUser(new User());
}
catch (Exception err)
{
err.Data.Add("SockInfo", "网络链接失败,请稍后再试");
throw err;
}
}
相当于把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]的更多相关文章
- 编写高质量代码改善C#程序的157个建议[1-3]
原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...
- 读书--编写高质量代码 改善C#程序的157个建议
最近读了陆敏技写的一本书<<编写高质量代码 改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...
- 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试
建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...
- 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本
建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...
- 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码
建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...
- 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣
建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...
- 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释
建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...
- 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释
建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...
- 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量
建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...
随机推荐
- Linux syslog介绍
一.简介 syslog是Linux系统默认的日志守护进程.默认的主配置文件和辅助配置文件分别是/etc/syslog.conf和/etc/sysconfig/syslog文件.通常,syslog 接受 ...
- WEB安全--Google Hacking
通常我们用Google查询一些我们测试站点的一些信息,Google提供了一系列的搜索语句,下面我为大家详细的介绍一下! 常用语法: site:指定域名 intext:正文中存在关键字的网页 intit ...
- 关于comparable与comparator的用法(即自定义集合框架用法 )
package javastudy; import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; ...
- MySQL数据库学习笔记(九)----JDBC的ResultSet接口(查询操作)、PreparedStatement接口重构增删改查(含SQL注入的解释)
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...
- 在spring环境下集成ActiveMQ
1.参考文献 Spring集成ActiveMQ配置 Spring JMS异步发收消息 ActiveMQ 2.环境 在前面的一篇ActiveMQ入门实例中我们实现了消息的异步传送,这篇博文将如何在spr ...
- java 16 - 5 LinkedList模拟栈数据结构的集合
请用LinkedList模拟栈数据结构的集合,并测试 题目的意思是: 你自己的定义一个集合类,在这个集合类内部可以使用LinkedList模拟. package cn_LinkedList; impo ...
- java10-3 equals方法
public boolean equals(Object obj):指示其他某个对象是否与此对象“相等”. 该方法,默认情况下比较的是地址值.但是,如果只是比较地址值的话,一般来说意义不大,所以要 ...
- R语言-merge和rbind
rbind 使用方式 合并两个数据集,要求两个数据集的列数相等: rbind(parameter1,parameter2) 1 1 合并多个数据集,各个数据集的列数相等: rbind(paramete ...
- VS的代码分析工具
来自:[译]Visual Studio 2008 Code Metrics http://www.cnblogs.com/live41/archive/2010/02/08/1665627.html ...
- C# Winform关于控件TabControl闪烁的问题
自己重写了一个Form,然后再该form上放一个TabControl鼠标移上去会闪烁,经过网上查找解决方案,最后总算是解决了....下面附上代码: 重写一个TabControl代码如下: using ...