关于C#:捕获和重新抛出.NET异常的最佳实践

Best practices for catching and re-throwing .NET exceptions

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

1
2
3
4
5
6
7
8
try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

Vs:

1
2
3
4
5
6
7
8
try
{
    //some code
}
catch
{
    throw;
}

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

1
2
3
4
5
6
try {
  // something that bombs here
} catch (Exception ex)
{
    throw;
}

throw ex;基本上类似于从该点抛出一个异常,因此堆栈跟踪只会指向您发出throw ex;语句的地方。

Mike也是正确的,假设异常允许您传递异常(建议这样做)。

卡尔·塞古在他编写电子书的基础上也写了一篇关于异常处理的伟大文章,这是一本伟大的著作。

编辑:指向编程PDF基础的工作链接。只需在文本中搜索"exception"。


如果使用初始异常引发新的异常,也将保留初始堆栈跟踪。

1
2
3
4
5
try{
}
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}


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

1
2
3
4
5
6
7
8
9
10
11
12
try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

stacktrace将指示第54行引发了异常,尽管它是在第47行引发的。

1
2
3
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

由于它是私有方法,因此必须使用反射调用它:

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

我有一个缺点,那就是依赖一个私有方法来保存stacktrace信息。它可以在.NET Framework的未来版本中更改。上面的代码示例和下面的解决方案是从FabriceMarguerie的日志中提取的。

调用exception.setObjectData

下面的技术是由Anton Tykhyy提出的,作为C中的答案,我如何在不丢失堆栈跟踪问题的情况下重新发送InnerException。

1
2
3
4
5
6
7
8
9
10
11
12
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
}

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

1
2
3
4
protected Exception(
    SerializationInfo info,
    StreamingContext context
)

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


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


经验法则是避免捕捉和投掷基本的Exception物体。这迫使您对异常更聪明一些;换句话说,您应该对SqlException有一个明确的捕获,这样您的处理代码就不会对NullReferenceException做任何错误。

但在现实世界中,捕获和记录基本异常也是一个好的实践,但不要忘记走完整的路去获取它可能拥有的任何InnerExceptions


没有人解释过ExceptionDispatchInfo.Capture( ex ).Throw()和普通throw之间的区别,所以这里是。然而,一些人注意到了throw的问题。

重新引发捕获的异常的完整方法是使用ExceptionDispatchInfo.Capture( ex ).Throw()(仅从.NET 4.5中提供)。

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

1。

1
2
3
4
5
6
7
8
9
10
11
void CallingMethod()
{
    //try
    {
        throw new Exception("TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2。

1
2
3
4
5
6
7
8
9
10
11
12
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.
    }
}

三。

1
2
3
4
5
6
7
8
9
10
11
void CallingMethod()
{
    try
    {
        throw new Exception("TEST" );
    }
    catch
    {
        throw;
    }
}

4。

1
2
3
4
5
6
7
8
9
10
11
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"和"throw ex"可能会做同样的事情,但他们没有给你一个关键的信息,这就是发生异常的那条线。

请考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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"操作时,您会得到堆栈跟踪,但第行将是22,因此您无法确定到底是哪一行引发了异常(除非您在try块中只有一行或几行代码)。要获得异常中预期的第17行,您必须使用原始异常堆栈跟踪抛出一个新的异常。


应始终使用"throw;"重新引发.NET中的异常,

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

基本上,msil(cil)有两条指令——"throw"和"rethrow":

  • C的"throw ex";编译成MSIL的"throw"
  • C的"throw;"-进入msil"rethrow"!

基本上,我可以看到为什么"throw-ex"会覆盖堆栈跟踪。


您还可以使用:

1
2
3
4
5
6
7
8
try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

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


我肯定会用:

1
2
3
4
5
6
7
8
9
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;
}

这将保留您的堆栈。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    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()报告的行号是throw;语句的行,因此您无法判断对bar()的调用中哪一个导致了异常。