C#规范整理·异常与自定义异常

<div style="background-color:#226DDD;width:100%;padding:10px;auto;text-indent:2em"><font color=#FFFFFF face="Microsoft YaHei" style="font-size:13px"> 这里会列举在C#中处理CLR异常方面的规范,帮助你们构建和开发一个运行良好和可靠的应用系统。 </font></div> ![](https://img2018.cnblogs.com/blog/710776/201906/710776-20190613112138058-2026075746.png) <h2 style="background-color:#226DDD;width:100%;color:#FFF;text-indent:1em">前言</h2>html

  迄今为止,CLR异常机制让人关注最多的一点就是“效率”问题。其实,这里存在认识上的误区,由于正常控制流程下的代码运行并不会出现问题,只有引起异常时才会带来效率问题。基于这一点,不少开发者已经达成共识:**不该将异常机制用于正常控制流中。**达成的另外一个共识是:CLR异常机制带来的“效率”问题不足以“抵消”它带来的巨大收益。 CLR异常机制至少有如下几个优势:程序员

  • 正常控制流会被当即停止,无效值或状态不会在系统中继续传播。
  • 提供了统一处理错误的方法。
  • 提供了在构造函数、操做符重载及属性中报告异常的便利机制。
  • 提供了异常堆栈,便于开发者定位异常发生的位置。

  另外,“异常”其名称自己就说明了它的发生是一个小几率事件。因此,因异常带来的效率问题会被限制在一个很小的范围内。实际上,try catch所带来的效率问题几乎是能够忽略的。在某些特定的场合,如Int32的Parse方法中,确实存在着由于滥用而致使的效率问题。在这种状况下,咱们就应该考虑提供一个TryParse方法,从设计的角度让用户选择让程序运行得更快。另外一种规避由于异常而影响效率的方法是:Tester-doer模式数据库

<h2 style="background-color:#226DDD;width:100%;color:#FFF;text-indent:1em">正文</h2> ### 1.用抛出异常代替返回错误代码 在异常机制出现以前,应用程序广泛采用返回错误代码的方式来通知调用者发生了异常。本建议首先阐述为何要用抛出异常的方式来代替返回错误代码的方式。对于一个成员方法而言,它要么执行成功,要么执行失败。成员方法执行成功的状况很容易理解,可是若是执行失败了却没有那么简单,由于咱们须要将致使执行失败的缘由通知调用者。抛出异常和返回错误代码都是用来通知调用者的手段。网络

可是当咱们想要告诉调用者更多细节的时候,就须要与调用者约定更多的错误代码。因而咱们很快就会发现,错误代码飞速膨胀,直到看起来彷佛没法维护,由于咱们总在查找并确认错误代码。 在没有异常处理机制以前,咱们只能返回错误代码。可是,如今有了另外一种选择,即便用异常机制。若是使用异常机制,那么最终的代码看起来应该是下面这样的:多线程

static void Main(string[]args)
{    
try  
    {   
     SaveUser(user); 
    }    
catch(IOException)   
    {       
    //IO异常,通知当前用户 
    }    
catch(UnauthorizedAccessException)
    {       
    //权限失败,通知客户端管理员  
    }    
catch(CommunicationException) 
    {        
   //网络异常,通知发送E-mail给网络管理员  
    }
}

private static void SaveUser(User user)
{   
  SaveToFile(user); 
  SaveToDataBase(user);
}

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

这里须要稍增强调的是,在catch(CommunicationExcep-tion)这个代码块中,代码所完成的功能是“通知发送”而不是“发送”自己,由于咱们要确保在catch和finally中所执行的代码是能够被执行的。换句话说,尽可能不要在catch和finally中再让代码“出错”,那会让异常堆栈信息变得复杂和难以理解。函数

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

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

2.不要在不恰当的场合下引起异常

程序员,尤为是类库开发人员,要掌握的两条首要原则是: 正常的业务流程不该使用异常来处理。 不要老是尝试去捕获异常或引起异常,而应该容许异常向调用堆栈往上传播。 那么,到底应该在怎样的状况下引起异常呢?spa

第一类状况 若是运行代码后会形成内存泄漏、资源不可用,或者应用程序状态不可恢复,则应该引起异常。 在微软提供的Console类中有不少相似这样的代码:

if((value<1)||(value>100))
{    
    throw new ArgumentOutOfRangeException("value",value, Environment.GetResourceString("ArgumentOutOfRange_CursorSize"));
}

或者:

if(value==null)
{    
  throw new ArgumentNullException("value");
}

在开头首先提到的就是:<font color=red>对在可控范围内的输入和输出不引起异常。没错,区别就在于“可控”这两个字。所谓“可控”,可定义为:发生异常后,系统资源仍可用,或资源状态可恢复。</font>

第二类状况 在捕获异常的时候,若是须要包装一些更有用的信息,则引起异常。 这类异常的引起在UI层特别有用。系统引起的异常所带的信息每每更倾向于技术性的描述;而在UI层,面对异常的极可能是最终用户。若是须要将异常的信息呈现给最终用户,更好的作法是先包装异常,而后引起一个包含友好信息的新异常。

第三类状况 若是底层异常在高层操做的上下文中没有意义,则能够考虑捕获这些底层异常,并引起新的有意义的异常。 例如在下面的代码中,若是抛出InvalidCastException,则没有任何意义,甚至会形成误解,因此更好的方式是抛出一个ArgumentException:

private 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"); 
}  

//do something}

须要重点介绍的正确引起异常的典型例子就是捕获底层API错误代码,并抛出。查看Console这个类,还会发现不少地方有相似的代码:

int errorCode=Marshal.GetLastWin32Error();
if(errorCode==6)
{   
  throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ConsoleKeyAvailableOnFile"));
}

Console为咱们封装了调用Windows API返回的错误代码,而让代码引起了一个新的异常。

很显然,当须要调用Windows API或第三方API提供的接口时,若是对方的异常报告机制使用的是错误代码,最好从新引起该接口提供的错误,由于你须要让本身的团队更好地理解这些错误。

3.从新引起异常时使用Inner Exception

当捕获了某个异常,将其包装或从新引起异常的时候,若是其中包含了Inner Exception,则有助于程序员分析内部信息,方便代码调试。 以一个分布式系统为例,在进行远程通讯的时候,可能会发生的状况有: 1)网卡被禁用或网线断开,此时会抛出SocketException,消息为:“因为目标计算机积极拒绝,没法链接。” 2)网络正常,可是要链接的目标机没有端口没有处在侦听状态,此时,会抛出SocketException,消息为:“因为链接方在一段时间后没有正确答复或链接的主机没有反应,链接尝试失败。” 3)链接超时,此时须要经过代码实现关闭链接,并抛出一个SocketException,消息为:“链接超过约定的时长。” 发生以上三种状况中的任何一种状况,在返回给最终用户的时候,咱们都须要将异常信息包装成为“网络链接失败,请稍候再试”。

因此,一个分布式系统的业务处理方法,看起来应该是这样的:

try
{    
SaveUser5(user);
}
catch(SocketException err)
{  
  throw new CommucationFailureException("网络链接失败,请稍后再试",err);
}

可是,在提示这条消息的时候,咱们可能须要将原始异常信息记录到日志里,以供开发者分析具体的缘由(由于若是这种状况频繁出现,这有多是一个Bug)。那么,在记录日志的时候,就很是有必要记录致使此异常出现的内部异常或是堆栈信息。 上文代码中的:就是将异常从新包装成为一个CommucationFailureException,并将SocketException做为Inner Exception(即err)向上传递。

此外还有一个能够采用的技巧,若是不打算使用Inner Exception,可是仍然想要返回一些额外信息的话,可使用Exception的Data属性。以下所示:

try
{   
 SaveUser5(user);
}
catch(SocketException err)
{    
 err.Data.Add("SocketInfo","网络链接失败,请稍后再试");   
 throw err;
}

在上层进行捕获的时候,能够经过键值来获得异常信息:

catch(SocketException err)
{   
 Console.WriteLine(err.Data["SocketInfo"].ToString());
}

4.避免在finally内撰写无效代码

你应该始终认为finally内的代码会在方法return以前执行,哪怕return是在try块中。 C#编译器会清理那些它认为彻底没有意义的C#代码。

private static int TestIntReturnInTry()
{   
  int i;    
  try    
  {        
    return i=1;  
  } 
  
finally   
 {        
    i=2;      
   Console.WriteLine("\t将int结果改成2,finally执行完毕");   
 }
}

5.避免嵌套异常

应该容许异常在调用堆栈中往上传播,不要过多使用catch,而后再throw。过多使用catch会带来两个问题:

  • 代码更多了。这看上去好像你根本不知道该怎么处理异常,因此你总在不停地catch。
  • 隐藏了堆栈信息,使你不知道真正发生异常的地方。

嵌套异常会致使 调用堆栈被重置了。最糟糕的状况是:若是方法捕获的是Exception。因此也就是说,若是这个方法中还存在另外的异常,在UI层将永远不知道真正发生错误的地方。 除了第3点提到的须要包装异常的状况外,无端地嵌套异常是咱们要极力避免的。固然,若是真的须要捕获这个异常来恢复一些状态,而后从新抛出,代码看起来应该是这样的:

try{ 
  MethodTry();
}
catch(Exception)
{ 
   //工做代码   
 throw;
}

或者:

try
{    
 MethodTry();
}
catch
{    
  //工做代码 
   throw;
}

尽可能避免像下面这样引起异常:

catch(Exception err)
{   
 //工做代码  
  throw err;
}

直接throw err而不是throw将会重置堆栈信息。

6.避免“吃掉”异常

嵌套异常是很危险的行为,一不当心就会将异常堆栈信息,也就是真正的Bug出处隐藏起来。但这还不是最严重的行为,最严重的就是“吃掉”异常,即捕获,而后不向上层throw抛出。若是你不知道如何处理某个异常,那么千万不要“吃掉”异常,若是你一不当心“吃掉”了一个本该往上传递的异常,那么,这里可能诞生一个Bug,并且,解决它会很费周折。

避免“吃掉”异常,并非说不该该“吃掉”异常,而是这里面有个重要原则:该异常可被预见,而且一般状况它不能算是一个Bug。 好比有些场景存在你能够预见的但不重要的Exception,这个就不算一个bug。

7.为循环增长Tester-Doer模式而不是将try-catch置于循环内

若是须要在循环中引起异常,你须要特别注意,由于抛出异常是一个至关影响性能的过程。应该尽可能在循环当中对异常发生的一些条件进行判断,而后根据条件进行处理。

8.老是处理未捕获的异常

处理未捕获的异常是每一个应用程序应具有的基本功能,C#在AppDomain提供了UnhandledException事件来接收未捕获到的异常的通知。常见的应用以下:

static void Main(string[]args)
{    
  AppDomain.CurrentDomain.UnhandledException+=new  UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}

static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{    
  Exception error=(Exception)e.ExceptionObject;   
 Console.WriteLine("MyHandler caught:"+error.Message);
}

未捕获的异常一般就是运行时期的Bug,咱们能够在App-Domain.CurrentDomain.UnhandledException的注册事件方法CurrentDomain_UnhandledException中,将未捕获异常的信息记录在日志中。值得注意的是,UnhandledException提供的机制并不能阻止应用程序终止,也就是说,执行CurrentDomain_UnhandledException方法后,应用程序就会被终止。

9.正确捕获多线程中的异常

多线程的异常处理须要采用特殊的方法。如下的处理方式会存在问题:

try{  
  Thread t=new Thread((ThreadStart)delegate    
{        
  throw new Exception("多线程异常");    
});   

 t.Start();
}

catch(Exception error)
{ 
   MessageBox.Show(error.Message+Environment.NewLine+error.StackTrace);
}

应用程序并不会在这里捕获线程t中的异常,而是会直接退出。从.NET 2.0开始,任何线程上未处理的异常,都会致使应用程序的退出(先会触发AppDomain的UnhandledException)。上面代码中的try-catch实际上捕获的仍是当前线程的异常,而t属于新起的异常,因此,正确的作法应该是把 try-catch放在线程里面

Thread t=new Thread((ThreadStart)delegate
{    
try   
 {      
  throw new Exception("多线程异常");   
 }    
catch(Exception error)   {  ....   });

t.Start();

10.慎用自定义异常

除非有充分的理由,不然通常不要建立自定义异常。若是要对某类程序出错信息作特殊处理,那就自定义异常。须要自定义异常的理由以下: 1)方便调试。经过抛出一个自定义的异常类型实例,咱们可使捕获代码精确地知道所发生的事情,并以合适的方式进行恢复。 2)逻辑包装。自定义异常可包装多个其余异常,而后抛出一个业务异常。 3)方便调用者编码。在编写本身的类库或者业务层代码的时候,自定义异常可让调用方更方便处理业务异常逻辑。例如,保存数据失败能够分红两个异常“数据库链接失败”和“网络异常”。 4)引入新异常类。这使程序员可以根据异常类在代码中采起不一样的操做。

11.从System.Exception或其余常见的基本异常中派生异常

这个不说了,自定义异常通常是从System.Exception派生。。事实上,如今若是你在Visual Studio中输入Exception,而后使用快捷键Tab,VS会自动建立一个自定义异常类。

12.应使用finally避免资源泄漏

前面已经提到过,除非发生让应用程序中断的异常,不然finally老是会先于return执行。finally的这个语言特性决定了资源释放的最佳位置就是在finally块中;另外,资源释放会随着调用堆栈由下往上执行(即由内到外释放)。

13.避免在调用栈较低的位置记录异常

即避免在内部深到处理记录异常。最适合记录异常和报告的是应用程序的最上层,这一般是UI层。 并非全部的异常都要被记录到日志,一类状况是异常发生的场景须要被记录,还有一类就是未被捕获的异常。未被捕获的异常一般被视为一个Bug,因此,对于它的记录,应该被视为系统的一个重要组成部分。

若是异常在调用栈较低的位置被记录或报告,而且又被包装后抛出;而后在调用栈较高位置也捕获记录异常。这就会让记录重复出现。在调用栈较低的状况下,每每异常被捕获了也不能被完整的处理。因此,综合考虑,应用程序在设计初期,就应该为开发成员约定在何处记录和报告异常。

總結

推薦看一下篇! 《多綫程/異步/并行/任務》

原文出处:https://www.cnblogs.com/zhan520g/p/11072519.html

相关文章
相关标签/搜索