【问题标题】:Best practices for catching and re-throwing .NET exceptions捕获和重新抛出 .NET 异常的最佳实践
【发布时间】:2010-09-06 13:29:45
【问题描述】:

在捕获异常并重新抛出异常时要考虑哪些最佳做法?我想确保保留 Exception 对象的 InnerException 和堆栈跟踪。以下代码块的处理方式有区别吗?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

对比:

try
{
    //some code
}
catch
{
    throw;
}

【问题讨论】:

    标签: c# .net exception-handling rethrow


    【解决方案1】:

    保留堆栈跟踪的方法是使用throw; 这也是有效的

    try {
      // something that bombs here
    } catch (Exception ex)
    {
        throw;
    }
    

    throw ex; 基本上就像从那时开始抛出异常,因此堆栈跟踪只会转到您发出 throw ex; 语句的位置。

    Mike 也是正确的,假设异常允许你传递异常(这是推荐的)。

    Karl Seguin 在他的foundations of programming e-book 中也有一个great write up on exception handling,非常适合阅读。

    编辑:Foundations of Programming pdf 的工作链接。只需在文本中搜索“异常”即可。

    【讨论】:

    • 我不太确定这篇文章是否精彩,它建议尝试 { // ... } catch(Exception ex) { throw new Exception(ex.Message + "other stuff" ); } 很好。问题是您完全无法在堆栈中进一步处理该异常,除非您捕获所有异常,这是一个很大的禁忌(您确定要处理该 OutOfMemoryException 吗?)
    • @ljs 自从您发表评论以来,文章是否已更改,因为我没有看到他推荐的任何部分。事实上恰恰相反,他说不要这样做,并问你是否也想处理 OutOfMemoryException!?
    • 有时 throw; 不足以保留堆栈跟踪。这是一个例子https://dotnetfiddle.net/CkMFoX
    • ExceptionDispatchInfo.Capture(ex).Throw(); throw; in .NET +4.5 stackoverflow.com/questions/57383/…
    【解决方案2】:

    如果你抛出一个带有初始异常的新异常,你也将保留初始堆栈跟踪。

    try{
    } 
    catch(Exception ex){
         throw new MoreDescriptiveException("here is what was happening", ex);
    }
    

    【讨论】:

    • 无论我尝试什么 throw new Exception("message", ex) 总是抛出 ex 并忽略自定义消息。 throw new Exception("message", ex.InnerException) 虽然有效。
    • 如果不需要自定义异常,可以使用 AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
    • AggregateException 应该只用于聚合操作的异常。例如,它由 CLR 的 ParallelEnumerableTask 类抛出。用法大概应该遵循这个例子。
    【解决方案3】:

    实际上,throw 语句在某些情况下不会保留 StackTrace 信息。例如,在下面的代码中:

    try
    {
      int i = 0;
      int j = 12 / i; // Line 47
      int k = j + 1;
    }
    catch
    {
      // do something
      // ...
      throw; // Line 54
    }
    

    StackTrace 将表明第 54 行引发了异常,尽管它是在第 47 行引发的。

    Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
       at Program.WithThrowIncomplete() in Program.cs:line 54
       at Program.Main(String[] args) in Program.cs:line 106
    

    在上述情况下,有两个选项可以保留原始 StackTrace:

    调用 Exception.InternalPreserveStackTrace

    由于是私有方法,所以必须使用反射来调用:

    private static void PreserveStackTrace(Exception exception)
    {
      MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
        BindingFlags.Instance | BindingFlags.NonPublic);
      preserveStackTrace.Invoke(exception, null);
    }
    

    我的缺点是依赖私有方法来保存 StackTrace 信息。它可以在 .NET Framework 的未来版本中进行更改。上面的代码示例和下面提出的解决方案摘自Fabrice MARGUERIE weblog

    调用 Exception.SetObjectData

    Anton Tykhyy 建议使用以下技术作为对In C#, how can I rethrow InnerException without losing stack trace 问题的回答。

    static void PreserveStackTrace (Exception e) 
    { 
      var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ; 
      var mgr = new ObjectManager     (null, ctx) ; 
      var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ; 
    
      e.GetObjectData    (si, ctx)  ; 
      mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
      mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData 
    
      // voila, e is unmodified save for _remoteStackTraceString 
    } 
    

    虽然它具有仅依赖于公共方法的优点,但它还依赖于以下异常构造函数(第三方开发的某些异常未实现):

    protected Exception(
        SerializationInfo info,
        StreamingContext context
    )
    

    在我的情况下,我不得不选择第一种方法,因为我使用的第 3 方库引发的异常没有实现这个构造函数。

    【讨论】:

    • 您可以捕获异常并将此异常发布到您想要的任何地方。然后抛出一个新的解释发生在用户身上的事情。这样你可以看到当前异常被捕获时发生了什么,用户可以忽略实际异常是什么。
    • 在 .NET 4.5 中还有第三个——在我看来——更简洁的选项:使用 ExceptionDispatchInfo。请参阅 Tragedians 对相关问题的回答:stackoverflow.com/a/17091351/567000 了解更多信息。
    • 这里一个普通的throw; 显示第 54 行而不是第 47 行可能应该被认为是 .NET Framework 中的一个长期存在的错误,并在 .NET Core 2.1 (github.com/dotnet /runtime/issues/9518)。您可以使用ExceptionDispatchInfo,但这不是它的主要用例之一(stackoverflow.com/a/17091351 中显示了其中一个),这表明它会使水变得混乱并导致代码可读性降低。也就是说,从 catch 中获取行,以及调用堆栈中的任何其他行号对我来说已经足够了。
    【解决方案4】:

    当您throw ex 时,您实际上是在抛出一个新异常,并且会错过原始堆栈跟踪信息。 throw 是首选方法。

    【讨论】:

      【解决方案5】:

      经验法则是避免捕获和抛出基本的Exception 对象。这迫使你对异常更聪明一点;换句话说,您应该明确地捕获 SqlException,这样您的处理代码就不会对 NullReferenceException 造成错误。

      不过,在现实世界中,捕获并记录基本异常也是一种好习惯,但不要忘记遍历整个过程以获取它可能拥有的任何InnerExceptions。 p>

      【讨论】:

      • 我认为最好使用 AppDomain.CurrentDomain.UnhandledException 和 Application.ThreadException 异常来处理未处理的异常以进行日志记录。到处使用 big try { ... } catch(Exception ex) { ... } 块意味着很多重复。取决于您是否要记录已处理的异常,在这种情况下(至少最少)重复可能是不可避免的。
      • 加上使用这些事件意味着你记录所有未处理的异常,而如果你使用 big ol' try { ... } catch(Exception ex) { ... }你可能会错过一些块。
      【解决方案6】:

      没有人解释过ExceptionDispatchInfo.Capture( ex ).Throw() 和普通的throw 之间的区别,所以在这里。不过也有人注意到throw的问题。

      重新抛出捕获的异常的完整方法是使用ExceptionDispatchInfo.Capture( ex ).Throw()(仅适用于 .Net 4.5)。

      下面是测试这个的必要案例:

      1.

      void CallingMethod()
      {
          //try
          {
              throw new Exception( "TEST" );
          }
          //catch
          {
          //    throw;
          }
      }
      

      2.

      void CallingMethod()
      {
          try
          {
              throw new Exception( "TEST" );
          }
          catch( Exception ex )
          {
              ExceptionDispatchInfo.Capture( ex ).Throw();
              throw; // So the compiler doesn't complain about methods which don't either return or throw.
          }
      }
      

      3.

      void CallingMethod()
      {
          try
          {
              throw new Exception( "TEST" );
          }
          catch
          {
              throw;
          }
      }
      

      4.

      void CallingMethod()
      {
          try
          {
              throw new Exception( "TEST" );
          }
          catch( Exception ex )
          {
              throw new Exception( "RETHROW", ex );
          }
      }
      

      案例 1 和案例 2 将为您提供堆栈跟踪,其中 CallingMethod 方法的源代码行号是 throw new Exception( "TEST" ) 行的行号。

      但是,案例 3 将为您提供堆栈跟踪,其中 CallingMethod 方法的源代码行号是 throw 调用的行号。这意味着如果throw new Exception( "TEST" ) 行被其他操作包围,您将不知道实际抛出异常的行号。

      情况 4 与情况 2 类似,因为保留了原始异常的行号,但不是真正的重新抛出,因为它改变了原始异常的类型。

      【讨论】:

      • 添加一个简单的简介,永远不要使用throw ex;,这是最好的答案。
      【解决方案7】:

      您应该始终使用“throw;”在 .NET 中重新抛出异常,

      参考这个, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

      基本上MSIL(CIL)有两条指令——“throw”和“rethrow”:

      • C# 的“throw ex;”被编译成 MSIL 的“抛出”
      • C# 的“投掷”; - 进入 MSIL “重新投掷”!

      基本上我可以看到“throw ex”覆盖堆栈跟踪的原因。

      【讨论】:

      【解决方案8】:

      有些人实际上错过了一个非常重要的点 - 'throw' 和 'throw ex' 可能会做同样的事情,但他们没有给你一个关键的信息,即异常发生的地方。

      考虑以下代码:

      static void Main(string[] args)
      {
          try
          {
              TestMe();
          }
          catch (Exception ex)
          {
              string ss = ex.ToString();
          }
      }
      
      static void TestMe()
      {
          try
          {
              //here's some code that will generate an exception - line #17
          }
          catch (Exception ex)
          {
              //throw new ApplicationException(ex.ToString());
              throw ex; // line# 22
          }
      }
      

      当您执行“throw”或“throw ex”时,您会得到堆栈跟踪,但 line# 将是 #22,因此您无法确定究竟是哪一行引发了异常(除非您只有try 块中的 1 行或几行代码)。要在异常中获得预期的第 17 行,您必须使用原始异常堆栈跟踪抛出一个新异常。

      【讨论】:

      • 这里一个普通的throw; 也显示了第 22 行而不是第 17 行,这应该被认为是 .NET Framework 中的一个长期存在的错误,并在 .NET Core 2.1 中得到了修复 (github.com/dotnet/runtime/issues/9518) .也就是说,我从来没有见过我关心在堆栈跟踪中看到来自try 的行的实例。从 catch 中获取行,以及调用堆栈中的任何其他行号总是足够好的。
      【解决方案9】:

      你也可以使用:

      try
      {
      // Dangerous code
      }
      finally
      {
      // clean up, or do nothing
      }
      

      并且抛出的任何异常都会冒泡到处理它们的下一个级别。

      【讨论】:

        【解决方案10】:

        我肯定会使用:

        try
        {
            //some code
        }
        catch
        {
            //you should totally do something here, but feel free to rethrow
            //if you need to send the exception up the stack.
            throw;
        }
        

        这将保留您的堆栈。

        【讨论】:

        • 为了公平起见,在 2008 年,OP 在询问如何保留堆栈——而 2008 年我给出了正确的答案。我的回答中缺少的是实际做某事的部分。
        • @JohnSaunders 当且仅当您在throw 之前做任何事情时才是正确的;例如,您可以清理一次性(仅在出现错误时调用它),然后抛出异常。
        • @meirion 当我写评论时,在投掷之前什么都没有。添加后,我投了赞成票,但没有删除评论。
        【解决方案11】:

        仅供参考,我刚刚测试了这个和'throw;'报告的堆栈跟踪不是完全正确的堆栈跟踪。示例:

            private void foo()
            {
                try
                {
                    bar(3);
                    bar(2);
                    bar(1);
                    bar(0);
                }
                catch(DivideByZeroException)
                {
                    //log message and rethrow...
                    throw;
                }
            }
        
            private void bar(int b)
            {
                int a = 1;
                int c = a/b;  // Generate divide by zero exception.
            }
        

        堆栈跟踪正确地指向异常的来源(报告的行号),但为 foo() 报告的行号是抛出的行;声明,因此您无法判断对 bar() 的哪些调用导致了异常。

        【讨论】:

        • 这就是为什么最好不要尝试捕获异常,除非你打算用它们做点什么
        猜你喜欢
        • 2011-07-29
        • 1970-01-01
        • 1970-01-01
        • 2021-02-16
        • 2010-12-02
        • 2011-06-13
        • 1970-01-01
        • 2011-12-24
        相关资源
        最近更新 更多