捕获和重新抛出 .NET 异常的最佳实践
-
09-06-2019 - |
题
捕获异常并重新抛出异常时要考虑的最佳实践是什么?我想确保 Exception
对象的 InnerException
和堆栈跟踪被保留。以下代码块处理此问题的方式有区别吗?
try
{
//some code
}
catch (Exception ex)
{
throw ex;
}
对比:
try
{
//some code
}
catch
{
throw;
}
解决方案
保存堆栈跟踪的方法是通过使用 throw;
这也是有效的
try {
// something that bombs here
} catch (Exception ex)
{
throw;
}
throw ex;
基本上就像从该点抛出异常,因此堆栈跟踪只会转到您发出异常的地方 throw ex;
陈述。
麦克风 也是正确的,假设异常允许您传递异常(推荐)。
卡尔·塞金 有一个 关于异常处理的精彩文章 在他的 编程基础电子书 这也是一本很棒的书。
编辑:工作链接到 编程基础 pdf。只需在文本中搜索“例外”即可。
其他提示
如果您抛出一个带有初始异常的新异常,您也将保留初始堆栈跟踪。
try{
}
catch(Exception ex){
throw new MoreDescriptiveException("here is what was happening", ex);
}
事实上,有一些情况是 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 的未来版本中更改。上面的代码示例和下面提出的解决方案摘自 法布里斯·玛格丽博客.
调用 Exception.SetObjectData
下面的技术是由 安东·泰克伊 作为答案 在 C# 中,如何在不丢失堆栈跟踪的情况下重新抛出 InnerException 问题。
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
)
在我的情况下,我必须选择第一种方法,因为我使用的第三方库引发的异常没有实现此构造函数。
当你 throw ex
, ,您实际上抛出了一个新的异常,并且会错过原始的堆栈跟踪信息。 throw
是首选方法。
经验法则是避免接住和投掷基本的 Exception
目的。这迫使你对异常更加聪明一些;换句话说,你应该有一个明确的捕获 SqlException
这样你的处理代码就不会做错事 NullReferenceException
.
但在现实世界中,捕捉 和记录 基本异常也是一个很好的实践,但不要忘记遍历整个过程以获得任何结果 InnerExceptions
可能有。
没有人解释过两者之间的区别 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
method 是行号 throw new Exception( "TEST" )
线。
但是,情况 3 将为您提供堆栈跟踪,其中源代码行号 CallingMethod
method 是行号 throw
称呼。这意味着如果 throw new Exception( "TEST" )
该行被其他操作包围,您不知道实际抛出异常的行号。
情况 4 与情况 2 类似,因为原始异常的行号被保留,但不是真正的重新抛出,因为它改变了原始异常的类型。
有些人实际上错过了一个非常重要的点 - ' 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”时,您会得到堆栈跟踪,但行#将是#22,因此您无法弄清楚到底哪一行抛出了异常(除非您只有1个或几个) try 块中的代码行)。要在异常中获得预期的第 17 行,您必须使用原始异常堆栈跟踪抛出一个新异常。
在 .NET 中,您应该始终使用 "throw; "来重新抛出异常、
参考这个,http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx
基本上 MSIL (CIL) 有两个指令 - “抛出”和“重新抛出”:
- c#的“投掷ex;”被编译成Msil的“掷”
- C#的“掷;” - 进入MSIL“ Rethrow”!
基本上我可以看到“throw ex”覆盖堆栈跟踪的原因。
您还可以使用:
try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}
抛出的任何异常都会上升到处理它们的下一个级别。
我肯定会使用:
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; "报告的堆栈跟踪并不完全正确。例子:
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() 的调用导致了异常。