《软件开发性能优化系列》之异常处理

     

     查看《软件开发性能优化系列》目录

 

     异常也是现代语言的典型特征。与传统检查错误码的方式相比,异常是强制性的(不依赖于是否忘记了编写检查错误码的代码)、强类型的、并带有丰富的异常信息(例如调用栈)。

1、不要吃掉异常

      关于异常处理的最重要原则就是:不要吃掉异常。这个问题与性能无关,但对于编写健壮和易于排错的程序非常重要。这个原则换一种说法,就是不要捕获那些你不能处理的异常。例如:

private void OnItemStatusSave_Extend(Object sender, UIActionEventArgs e)
{
        try
       {
          this.CommonAction.save(this.VurrentModel.ItemMaster_Status.ContainerModel);
       }
       catch(Exception)
       {
       }
}


     
      吃掉异常是极不好的习惯,因为你消除了解决问题的线索。一旦出现错误,定位问题将非常困难。除了这种完全吃掉异常的方式外,只将异常信息写入日志文件但并不做更多处理的做法也同样不妥。

PageDate pageDefine = null;
CSPage page = null;
try
{
     pageDefineBp.PageURI = key.ToString();
     pageDefine = pageDefineBp.Do();

     page = new CSPage();
     page.Id =pageDefine.ID.ToString();
     page.Name=pageDefine.Name;
     page.Title = pageDefine.Title;
     page.Orientation = pageDefine.Orientation;
}
catch(Exception ex)
{
     logger.Error("PageLoaderProxy ErrorL"+ ex.StackTrace);
}
if(pageDefine.PartRelation != null)
{
      foreach(PartRelationData pRelation in pageDefine.PartRelation);
}

      如上例,一旦调用方法是不,PageDefine 为 null,随后访问pageDefine.PartRelation将抛出NullReferenceException错误,这与原始的异常完全不一样。例如,原始的异常信息可能指示数据库配置字符串有错,简单更改后就OK了。而象现在这样,如果没有源代码,定位问题将很困难。


2、不要吃掉异常信息


      有些代码虽然抛出了异常,但却把异常信息吃掉了。例如吓例,后台调用异常的原因可能是因为编码输入重复,未能通过数据库唯一性约束。但展现的异常信息却是一句毫无意义的废话,原始的异常信息被吃掉了。本来只需要用户把编码修改一下,重新提交就OK了,但现在不得不依靠程序员来协助排错。
为异常披露详尽的信息是程序员的职责所在。如果不能在保留原始异常信息含义的前提下附加更丰富和更人性化的内容,那么让原始的异常信息直接展示也要强得多。千万不要吃掉异常。

 
3、 避免不必要的抛出异常


      抛出异常和捕获异常属于消耗比较大的操作,在可能的情况下,应通过完善程序逻辑避免抛出不必要不必要的异常。例如:

private IComplexType GetComplexType(Key key)
{
     if(key == null) return null;
     if(key.Equals(Key.Empty)) return null;
     try
     {
          if(parentEntity != null)
                type = (IComplexType)this.parentEntity.Namespace.Component.FIndTypeBykey(key);
      }
      catch(Exception ex)
       {
            //logger.Error("association({0}) load type from designtime error{1}",this.ToString())
       }
}

      如果this.parentEntity.Namespace等于null ,此方法运行期间会抛出许多NullReferenceException,应通过增加对Namespace是否为null的检查,避免抛出异常。

与此相关的一个倾向是利用异常来控制处理逻辑。尽管对于极少数的情况,这可能获得更为优雅的解决方案,但通常而言应该避免。 

4、 避免不必要的重新抛出异常


      如果是为了包装异常的目的(即加入更多信息后包装成新异常),那么是合理的。但是有不少代码,捕获异常没有做任何处理就再次抛出,这将无谓地增加一次捕获异常和抛出异常的消耗,对性能有伤害。例如:

private void OnDeleteCommon(IUIView mView)
{
   try
   {
      if(this.MainView.Fields["IsSelectList"] != null)
      {
           //遍历所有checkbox==true的record
           ……
       }
      else
      {
           ……
      }
      //最后置状态
   this.CurrentPart.PageStatus = UFSoft.UBF.UI.IView.PartStateType.Unchanged;
   }
   catch(Exception ex)
   {
       throw;
   }
}

 

将这个原则与“不要吃掉异常”比较起来,不要吃掉异常更为重要!例如下例:

try
{
     user = Provider.GetUser(username, isOnline, lastAction);
}
catch(Exception ex)
{
     //Fixed;读取用户信息数据库异常处理;
}

       程序员一句清楚应该在以后补上异常处理动作,但由于吃掉了异常,会给排错代理极大困难,此时增加一个throw语句要比吃掉异常强的多!

 

5、使用throw而不是throw e抛出原来的异常

 

如果在捕获异常之后,不必包装成新的异常,而只是继承抛出原来的异常,通常可以看到两种写法。

方式1:使用布袋参数的throw语句。示例如下:

try
{
    int val = objMember.getMember();
}
catch(Exception ex)
{
     throw;
}

方式2:使用带对象实例的throw语句,示例如下:

try
{
    int val = objMember.getMember();
}
catch(Exception ex)
{
     throw e;
}

      方式2表面上看跟方式一没有什么不同,其实有很微妙的差异:throw e 表示在当前位置重新抛出异常,并不是转发原来的异常。它会更改异常的内部信息,最主要的是StackTrace会发生变化,引发异常的代码位置将变为throw e代码所在的位置而非最初引发异常的代码位置!这应该不是希望看到的结果吧,如果查看IL代码会发现一个是rethrow指令,另一个则是throw指令,完全不同。

      另外,方式2在性能上也有较大损失,因为异常最主要的消耗不是在new Exception的时候,而是在throw Exception的时候,因为throw语句才会更改包括StackTrace在内的许多异常内部信息。对于复杂的应用,如果异常时的调用链很深,构造StockTrace的消耗之大远远超乎想象,我们在研究异常消耗时可能会写一个简单的控制台程序,模拟抛出异常,捕获异常来研究异常消耗,在这种场景下得到的印象是.net的异常性能还可以,但在真实的运用中你会看到这个消耗能上升2到3个数量级!

因此,综合考虑这两方面的因素,我们最好按照throw而不是throw e的方式抛出原来的异常

posted @ 2010-02-06 22:49  JoneLee  阅读(2305)  评论(0编辑  收藏  举报
http://s.click.taobao.com/t_9?p=mm_33531378_0_0&l=http%3A%2F%2Fwww.tmall.com%2Fgo%2Fact%2Fsale%2Ftmmytkpd.php%E8%81%BD