在c#中重新抛出异常的正确方法是什么?

这样做更好吗:

try
{
...
}
catch (Exception ex)
{
...
throw;
}

或:

try
{
...
}
catch (Exception ex)
{
...
throw ex;
}

他们做同样的事情吗?一个比另一个好吗?

222464 次浏览

您应该始终使用以下语法来重新抛出异常。否则你将停止堆栈跟踪:

throw;

如果你打印throw ex产生的跟踪,你会看到它结束于该语句,而不是异常的真正来源。

基本上,使用throw ex应该被视为刑事犯罪。


如果需要重新抛出来自其他地方(AggregateException, TargetInvocationException)或另一个线程的异常,也不应该直接重新抛出。相反,有ExceptionDispatchInfo保存所有必要的信息。

try
{
methodInfo.Invoke(...);
}
catch (System.Reflection.TargetInvocationException e)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e.InnerException).Throw();
throw; // just to inform the compiler that the flow never leaves the block
}

第一个保存异常的原始堆栈跟踪,第二个用当前位置替换它。

因此,第一个到目前为止越好。

第一个比较好。如果尝试调试第二个异常并查看调用堆栈,则无法看到原始异常来自何处。如果真的需要重新抛出,有一些技巧可以保持调用堆栈的完整性(尝试搜索,它之前已经被回答过了)。

我的偏好是使用

try
{
}
catch (Exception ex)
{
...
throw new Exception ("Add more context here", ex)
}

这保留了原来的错误,但是它允许您添加更多的上下文,例如对象ID、连接字符串等等。我的异常报告工具通常有五个链接的异常要报告,每个报告更详细。

如果你用__abc0变量抛出异常(第一个例子),堆栈跟踪将包括抛出异常的原始方法。

在第二个示例中,堆栈跟踪将被更改以反映当前方法。

例子:

static string ReadAFile(string fileName) {
string result = string.Empty;
try {
result = File.ReadAllLines(fileName);
} catch(Exception ex) {
throw;    // This will show ReadAllLines in the stack trace
throw ex; // This will show ReadAFile in the stack trace
}

视情况而定。在调试构建中,我希望尽可能少地查看原始堆栈跟踪。在这种情况下,“扔”;符合要求。

然而,在发布版本中,(a)我想记录包含原始堆栈跟踪的错误,一旦完成,(b)重新设计错误处理以使用户更有意义。这里“抛出异常”;是有意义的。重新抛出错误确实会丢弃原始的堆栈跟踪,但非开发人员从查看堆栈跟踪信息中得不到任何信息,因此可以重新抛出错误。

        void TrySuspectMethod()
{
try
{
SuspectMethod();
}
#if DEBUG
catch
{
//Don't log error, let developer see
//original stack trace easily
throw;
#else
catch (Exception ex)
{
//Log error for developers and then
//throw a error with a user-oriented message
throw new Exception(String.Format
("Dear user, sorry but: {0}", ex.Message));
#endif
}
}

这个问题的措辞,有点像“扔”;vs.“扔掉”;这就有点转移话题了。真正的选择是“扔”;和“抛出异常”;where &;Throw ex;&;是一个不太可能的“抛出例外”的特殊情况。

你应该总是使用“扔”;重新抛出。net中的异常,

参考博客文章Throw vs. Throw ex

基本上,MSIL (CIL)有两个指令"抛出"和“;rethrow"c#的“throw ex”;被编译成MSIL的&;throw&;c#的“throw”;-进入MSIL "rethrow"!基本上我可以看出为什么“throw ex"重写堆栈跟踪。

我发现,如果在捕获异常的同一方法中抛出异常,堆栈跟踪就不会被保留。

void testExceptionHandling()
{
try
{
throw new ArithmeticException("illegal expression");
}
catch (Exception ex)
{
throw;
}
finally
{
System.Diagnostics.Debug.WriteLine("finally called.");
}
}

我同意,大多数情况下,你要么想要做一个普通的throw,以保留尽可能多的关于出错的信息,要么你想要抛出一个新的异常,其中可能包含一个内部异常,或者不抛出,这取决于你想知道引起它的内部事件的可能性有多大。

但也有例外。在几种情况下,一个方法会调用另一个方法,而在内部调用中导致异常的条件应该被认为是外部调用中的相同异常。

一个例子是使用另一个集合实现的专门集合。假设它是一个DistinctList<T>,它包装了List<T>,但拒绝重复项。

如果有人在你的集合类上调用ICollection<T>.CopyTo,它可能只是在内部集合上直接调用CopyTo(如果说,所有自定义逻辑只应用于向集合添加或设置它)。现在,该调用抛出的条件与集合抛出以匹配ICollection<T>.CopyTo文档的条件完全相同。

现在,您完全可以不捕获异常,让它通过。不过,在这里,当用户调用DistinctList<T>上的东西时,会从List<T>得到一个异常。这并不是世界末日,但是您可能希望隐藏那些实现细节。

或者你也可以自己检查:

public CopyTo(T[] array, int arrayIndex)
{
if(array == null)
throw new ArgumentNullException("array");
if(arrayIndex < 0)
throw new ArgumentOutOfRangeException("arrayIndex", "Array Index must be zero or greater.");
if(Count > array.Length + arrayIndex)
throw new ArgumentException("Not enough room in array to copy elements starting at index given.");
_innerList.CopyTo(array, arrayIndex);
}

这并不是最糟糕的代码,因为它是样板代码,我们可以从CopyTo的其他实现中复制它,在那里它不是一个简单的传递,我们必须自己实现它。尽管如此,它仍然不必要地重复将在_innerList.CopyTo(array, arrayIndex)中完成的完全相同的检查,因此它向我们的代码中添加的唯一东西是可能存在错误的6行。

我们可以检查并括起来:

public CopyTo(T[] array, int arrayIndex)
{
try
{
_innerList.CopyTo(array, arrayIndex);
}
catch(ArgumentNullException ane)
{
throw new ArgumentNullException("array", ane);
}
catch(ArgumentOutOfRangeException aore)
{
throw new ArgumentOutOfRangeException("Array Index must be zero or greater.", aore);
}
catch(ArgumentException ae)
{
throw new ArgumentException("Not enough room in array to copy elements starting at index given.", ae);
}
}

就新添加的代码而言,可能会有bug,这就更糟糕了。我们从内部异常中得不到任何东西。如果我们将一个空数组传递给这个方法并接收到ArgumentNullException,我们不会通过检查内部异常了解到调用_innerList.CopyTo时会传递一个空数组并抛出ArgumentNullException

在这里,我们可以做任何我们想做的事情:

public CopyTo(T[] array, int arrayIndex)
{
try
{
_innerList.CopyTo(array, arrayIndex);
}
catch(ArgumentException ae)
{
throw ae;
}
}

如果用户使用不正确的参数调用它,我们期望抛出的每一个异常,都将由该重新抛出正确抛出。如果在这里使用的逻辑中有错误,它是在两行中的其中一行中——要么我们错误地决定了这是这种方法有效的情况,要么我们错误地将ArgumentException作为异常类型查找。这是catch块可能仅有的两个bug。

现在。我仍然同意,大多数情况下,你要么想要一个普通的throw;,要么想要构造自己的异常,以便从相关方法的角度更直接地匹配问题。在像上面这样的情况下,像这样的重新抛出更有意义,还有很多其他情况。例如,举一个非常不同的例子,如果一个使用FileStreamXmlTextReader实现的ATOM文件读取器接收到一个文件错误或无效的XML,那么它可能会抛出它从这些类中接收到的完全相同的异常,但它应该向调用者查找抛出FileNotFoundExceptionXmlException的是AtomFileReader,因此它们可能是类似地重新抛出的候选对象。

我们也可以将两者结合起来:

public CopyTo(T[] array, int arrayIndex)
{
try
{
_innerList.CopyTo(array, arrayIndex);
}
catch(ArgumentException ae)
{
throw ae;
}
catch(Exception ex)
{
//we weren't expecting this, there must be a bug in our code that put
//us into an invalid state, and subsequently let this exception happen.
LogException(ex);
throw;
}
}