在 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;,要么您想要构建自己的异常以从相关方法的角度更直接地匹配问题。在上述情况下,这样的重新抛出更有意义,还有很多其他情况。例如,举一个非常不同的例子,如果使用FileStreamXmlTextReader 实现的ATOM 文件阅读器接收到文件错误或无效的XML,那么它可能希望抛出与从这些类接收到的完全相同的异常,但它应该让调用者知道是 AtomFileReader 正在抛出 FileNotFoundExceptionXmlException,因此它们可能是类似重新抛出的候选者。

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

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++ 和 C# 中使用 try/catch 的正确方法是啥? [复制]

引发类型为“System.OutOfMemoryException”的异常.是啥错误

从异步函数传播异常的正确方法是啥? [复制]

是否有一些 Assert 方法可用于对方法是否引发异常进行单元测试? [复制]

XslCompiledTransform 中的 XSLT 复制和排序转换引发异常