【问题标题】:.NET Return value vs thrown Exception Design question.NET 返回值与抛出的异常设计问题
【发布时间】:2011-02-03 09:50:01
【问题描述】:

假设我们有一个方法处理树层次结构数据结构中的操作,该结构位于处理这种结构的类中。

让我们仔细看看其中一种方法:

void MoveNode(Node currentNode, Node newParentNode)
{
    /* check if new parent isn't current's child already */
     if (newParentNode.LMargin < currentNode.LMargin && newParentNode.RMargin > currentNode.RMargin)
     {
        //DO WORK
     }
     else throw new ArgumentException("New parent node cannot be current's own child");
}

MSDN 声明:不要抛出异常来控制流程!

我的问题:您认为 ArgumentException 的这种使用是否可行,或者您会使用某种返回值。 如果是这样,您将如何提供错误/验证消息。

【问题讨论】:

  • 就我个人而言,我什么也不做。对我来说,将move 放在它已经存在的同一个地方似乎并不无效,就像int i = 0; i = 0; 是无效的一样。
  • 逻辑是新的父母不能是当前的孩子。不是同一个地方。
  • 是的,这更有意义,抱歉我看错了:)
  • 这能回答你的问题吗? Choosing between exception and return value

标签: c# .net exception-handling return-value


【解决方案1】:

由于抛出的异常表明此处存在错误,因此永远不会在正常工作的程序中抛出异常,因此异常是正确的选择。

你不应该做的是:

try
{
   MoveNode(...)
   //Do something
}
catch(ArgumentException e)
{
  //Do something else
}

在该示例中,您希望定期抛出异常并使用它来控制控制流。在调用者中捕获ArgumentException 几乎总是一个坏主意。这种异常应该只在顶级处理程序中被捕获,如果有的话。

我个人不喜欢你在 else 子句中抛出异常。我更喜欢在函数开头进行参数检查,然后立即抛出异常。这样可以防止在多个 if 块中嵌套非错误代码。

有三种类型的异常

  1. StackOverflow、OutOfMemory 和 ThreadAborted 等异步异常。它们可能发生在任何地方,并且无法真正处理
  2. 像 ArgumentException 这样的错误异常,将它们记录在顶级处理程序中并修复错误
  3. 表示可以在本地处理的错误的预期异常。通常,当错误不常见时,您会使用它们,并且您无法事先知道操作会导致错误。 IO 错误就是一个典型的例子。
    原因通常是外部的。例如无法访问的文件、网络故障或您尝试解析的文件中的无效数据。

Eric Lippert 在一篇博文中谈到了这些类型的异常:Vexing exceptions

何时使用第三种异常,何时使用返回值是判断调用。

【讨论】:

  • 完全同意一开始就检查异常。
  • 好的,但如果用户界面允许此分配,这也意味着它是流程的一部分,并且我应该避免捕获 ArgumentException,我是否必须返回一个值来代替异常?
  • @Pan1c 要么抛出另一种类型的异常(ArgumentException 只能用于错误),要么使用返回值。哪个更好取决于您的功能的使用。如果错误的原因是外部的,通常会使用异常。在您的情况下,我将使用返回值。我添加了一些关于不同类型异常的信息。
  • 有时很难确定异常是 Eric 所说的“令人烦恼的异常”还是“外生异常”。经验对那个决定最有帮助,即使这样你有时也会出错。通常提供两种版本的函数,一种返回指示成功的错误代码/布尔值,另一种抛出异常。
【解决方案2】:

我不认为这是“不使用异常来控制流程”的情况。

这是参数验证。该方法无法工作输入参数无效,因此,使用异常是告诉调用者它正在尝试做坏事的最佳方式。

【讨论】:

  • 我不认为你试图告诉 caller 参数,而是 programmer 所以他修复了这个 bug。
  • 是的,您是在告诉调用者“嘿,您想以一种不好的方式使用我”,这是因为 ArgumentException,您在其中给出了错误的参数名称。我知道你几乎不会对“参数名称”做任何事情,但它会被记录下来,然后你会修复错误,因为调用者由于异常而被注意到。
  • (立即)调用者不应捕获或关心该异常。它应该只被通用顶级异常处理程序捕获。
  • @CodeInChors 现在我看到你认为我的建议是“异常将由调用者处理”。我在谈论理论,调用者被注意到了,但它不能处理整个异常。就是这样。
【解决方案3】:

如果是unexpected运行时错误,和这里一样,是异常,否则应该是返回值。在 int.Parse(抛出)/int.TryParse(返回值)上建模您的决定,第一个用于您知道事物必须是 int 的情况(例如解析类型化结构),另一个用于验证用户输入(键入错误期望来自用户)。

异常处理在抛出时运行时开销很大,应该在循环中避免。

【讨论】:

    【解决方案4】:

    与其问某事是否应该返回值或抛出异常,不如问一个函数承诺做什么。如果一个函数承诺移动一个节点,如果它不能移动它应该抛出一个异常。如果一个函数承诺在可能的情况下移动一个节点,或者通过返回值指示它不能移动,但只有在数据结构以某种方式从根本上破坏超出移动节点的能力所暗示的方式时才会抛出异常,它应该这样做。有时,同时提供“做”和“尝试”两种功能会很有用。

    至于抛出什么类型的异常,坦率地说,我不喜欢从用户代码中抛出大多数内置异常类型的概念,因为没有很好的编程方式来判断 ArgumentException 是从您的例程中抛出的,还是从某个例程中抛出的这是由您的例程调用的,大多数例外都没有说明底层数据结构的质量。

    如果有人尝试例如从磁盘解析文件并将其与现有数据结构集成并引发异常,无论异常是 ArgumentException、SubscriptOutOfBoundsException、DiskReadErrorException 还是其他任何异常。最重要的是解析尝试是否以使数据结构有效的方式回滚;次要重要的是是否有可能以另一种方式或在其他情况下解析文件。异常的类型实际上只在它能回答前两个问题的范围内起作用。

    【讨论】:

      猜你喜欢
      • 2017-10-04
      • 2014-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-29
      • 2012-12-22
      • 2012-06-08
      • 1970-01-01
      相关资源
      最近更新 更多