在 C# 中重新引发异常的正确方法是啥? [复制]
Posted
技术标签:
【中文标题】在 C# 中重新引发异常的正确方法是啥? [复制]【英文标题】:What is the proper way to rethrow an exception in C#? [duplicate]在 C# 中重新引发异常的正确方法是什么? [复制] 【发布时间】:2010-09-15 18:15:33 【问题描述】:这样做更好吗:
try
...
catch (Exception ex)
...
throw;
或者这个:
try
...
catch (Exception ex)
...
throw ex;
他们做同样的事情吗?一个比另一个好?
【问题讨论】:
【参考方案1】:您应该始终使用以下语法重新引发异常。否则你会踩到堆栈跟踪:
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
【讨论】:
有一个 fxcop 规则:RethrowToPreserveStackDetails msdn.microsoft.com/en-us/library/ms182363(VS.80).aspx 这是真的吗?我认为这些信息仍然存在于 innerException 中。哦,我明白了。 throw ex 确实会丢失信息,但 throw new Exception("foo", ex) 不会(它将在内部异常中)。 不能再同意了。有 5 年以上经验的开发人员并不真正了解异常处理最佳实践,这确实令人惊讶。当我看到 try/catch 要么在不记录的情况下吞下异常,要么重新抛出异常而不在 catch 中做任何有价值的事情时,我无法解释我有多想睁大眼睛。 有趣,不适合我。堆栈跟踪丢失。 throw ex 和 throw 没有区别,结果一样。 从 .Net 4.5 开始,必须使用的是:ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); ***.com/a/17091351/354756【参考方案2】:我的偏好是使用
try
catch (Exception ex)
...
throw new Exception ("Add more context here", ex)
这保留了原始错误,但它允许您添加更多上下文,例如对象 ID、连接字符串和类似的东西。我的异常报告工具通常会报告五个链接的异常,每个都报告更多详细信息。
【讨论】:
通常我会抛出特定的 BusinesssLayer 异常(例如 BusinessObjectInstantionFailed 之类的东西)。我更喜欢链式异常,因为我希望大多数其他开发人员认为异常可能是链式的,但他们可能不会询问其他属性。 这不会保留堆栈跟踪。 @kami 原始堆栈跟踪在内部异常中仍然可用。是包装异常还是只包装throw;
取决于具体情况。
同意 - 但是为什么你甚至会首先捕获异常,除非你可以执行一个有用的操作,比如向它添加上下文?问题的前提是此时无法处理异常。
我发现这种技术对于在一些递归 XML 反序列化代码中捕获和重新抛出异常特别有用。如果我捕获然后执行throw new Exception( string.Format("while deserializing the element 0", element.Name), ex );
,那么如果我在 XML 解析中遇到崩溃,***异常处理程序会打印所有 InnerExceptions,并且我会得到它当前所有嵌套 XML 节点名称的完整跟踪内部,仅通过查看调用堆栈就不会很明显,该调用堆栈只是对递归函数进行了很多调用。【参考方案3】:
如果您使用out变量引发异常(第二个示例),堆栈跟踪将包括引发异常的原始方法。
在第一个示例中,堆栈跟踪将被更改以反映当前方法。
例子:
static string ReadAFile(string fileName)
string result = string.Empty;
try
result = File.ReadAllLines(fileName);
catch(Exception ex)
throw ex; // This will show ReadAFile in the stack trace
throw; // This will show ReadAllLines in the stack trace
【讨论】:
【参考方案4】:第一个保留异常的原始堆栈跟踪,第二个将其替换为当前位置。
因此,第一个到目前为止更好。
【讨论】:
【参考方案5】:我同意大多数情况下,您要么想做一个简单的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);
就添加的可能存在错误的新代码而言,情况更糟。我们并没有从内部异常中获得任何东西。如果我们将一个空数组传递给此方法并接收一个ArgumentNullException
,我们将不会通过检查内部异常并了解对_innerList.CopyTo
的调用传递了一个空数组并抛出一个ArgumentNullException
来学习任何东西。
在这里,我们可以为所欲为:
public CopyTo(T[] array, int arrayIndex)
try
_innerList.CopyTo(array, arrayIndex);
catch(ArgumentException ae)
throw ae;
如果用户使用不正确的参数调用它,我们期望必须抛出的每一个异常都将被该重新抛出正确地抛出。如果此处使用的逻辑中存在错误,则它位于两行之一 - 要么我们错误地认为这是这种方法有效的情况,要么我们错误地将 ArgumentException
作为查找的异常类型。这是 catch 块中唯一可能存在的两个错误。
现在。我仍然同意大多数时候您要么想要一个普通的throw;
,要么您想要构建自己的异常以从相关方法的角度更直接地匹配问题。在上述情况下,这样的重新抛出更有意义,还有很多其他情况。例如,举一个非常不同的例子,如果使用FileStream
和XmlTextReader
实现的ATOM 文件阅读器接收到文件错误或无效的XML,那么它可能希望抛出与从这些类接收到的完全相同的异常,但它应该让调用者知道是 AtomFileReader
正在抛出 FileNotFoundException
或 XmlException
,因此它们可能是类似重新抛出的候选者。
我们也可以将两者结合起来:
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;
【讨论】:
没有。就是不行。抛出异常时不要隐藏“实现细节”。当他们在您的源中查找异常的解释并发现它本身不会生成异常时,这只会使人们头脑混乱。使用原始堆栈跟踪,您至少可以找到生成异常的点,以确切了解引发它的原因。 @SørenBoisen,当适当的异常来自适当的方法时,他们为什么需要查看源代码?如果不是这种情况,那么您就没有这种投掷有意义的极少数情况之一。 Re “有一个例外”:双关语是故意的? "an ATOM file reader"中的"ATOM"指的是什么?文本编辑器Atom? @PeterMortensen 它指的是基于 XLM 的 Atom Web 标准。 en.wikipedia.org/wiki/Atom_(Web_standard)【参考方案6】:您应该始终使用“throw;”在 .NET 中重新抛出异常,
参考博文Throw vs. Throw ex。
基本上,MSIL (CIL) 有两条指令——“throw”和“rethrow”以及 C# 的“throw ex;”。被编译成 MSIL 的 "throw" 和 C# 的 "throw;" - 进入MSIL“重投”!基本上我可以看到“throw ex”覆盖堆栈跟踪的原因。
【讨论】:
【参考方案7】:第一个更好。如果您尝试调试第二个并查看调用堆栈,您将看不到原始异常的来源。如果您确实需要重新抛出,有一些技巧可以保持调用堆栈完整(尝试搜索,之前已回答过)。
【讨论】:
【参考方案8】:我发现如果在捕获异常的同一方法中抛出异常,则不会保留堆栈跟踪,因为它的价值。
void testExceptionHandling()
try
throw new ArithmeticException("illegal expression");
catch (Exception ex)
throw;
finally
System.Diagnostics.Debug.WriteLine("finally called.");
【讨论】:
【参考方案9】:这取决于。在调试版本中,我希望尽可能少地查看原始堆栈跟踪。在这种情况下,“投掷”;符合要求。
然而,在发布版本中,(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
问题的措辞方式,将“Throw:”与“Throw ex;”相提并论。让它有点像红鲱鱼。真正的选择是在“投掷”之间。和“Throw Exception”,其中“Throw ex;”是“抛出异常”的一个不太可能的特例。
【讨论】:
-1 用于编译器条件。 将 UI 消息放在 Exception 消息中也是一种非常紧密的耦合。不利于可维护性。 @EricJ。如果它在异常消息中,那么“用户”是指使用该类的开发人员,而不是维护它的人。这正是异常消息的受众。 为什么此时需要捕获异常呢?只需在根目录捕获异常(或添加全局处理程序),向用户显示对话框,将ex.ToString()
记录到应用程序日志中,等等。而且您无需为是否正在运行调试版本而烦恼。以上是关于在 C# 中重新引发异常的正确方法是啥? [复制]的主要内容,如果未能解决你的问题,请参考以下文章
在 C++ 和 C# 中使用 try/catch 的正确方法是啥? [复制]
引发类型为“System.OutOfMemoryException”的异常.是啥错误