为什么说Exceptions对于输入验证是如此糟糕?

时间:2009-01-04 06:25:33

标签: exception error-handling validation

我理解“异常是针对例外情况”[a],但除了再次重复overover之外,我从来没有找到这个事实的实际原因。

由于他们停止执行,你不希望它们用于普通的条件逻辑,但为什么不输入验证呢?

假设您要遍历一组输入并捕获每个异常以将它们组合在一起以供用户通知...我不断看到这是“错误的”,因为用户一直输入错误的输入,但这一点似乎是based on semantics

输入不是预期的,因此是例外。抛出异常允许我准确定义错误的StringValueTooLong或IntegerValueTooLow或InvalidDateValue等等。为什么这被认为是错误的?

抛出异常的替代方法是返回(并最终收集)错误代码或更糟糕的错误字符串。然后我会直接显示这些错误字符串,或者解析错误代码然后向用户显示相应的错误消息。不会将异常视为可延展的错误代码吗?为什么要创建一个单独的错误代码和消息表,何时可以使用我的语言中已经内置的异常功能进行推广?

另外,我found this article by Martin Fowler关于如何处理这些事情 - 通知模式。我不确定我是如何将此视为除了不停止执行的异常之外的任何东西。

a:我到处都读过有关例外情况的内容。

---编辑---

已经提出了许多重点。我对大多数评论都很好,而且还有很好的观点,但我还没有完全相信。

我并不是说将Exceptions作为解决输入验证的正确方法,但我想找到很好的理由,为什么这种做法被认为是如此邪恶,因为似乎大多数替代解决方案只是伪装的例外。

17 个答案:

答案 0 :(得分:31)

阅读这些答案,我发现说“异常只能用于特殊条件”是非常无益的。这引出了什么是“特殊情况”的整个问题。这是一个主观术语,其最佳定义是“您的正常逻辑流程不处理的任何条件”。换句话说,异常条件是您使用异常处理的任何条件。

我对此作为一个定义很好,我不知道我们会比这更接近。但是你应该知道这就是你正在使用的定义。

如果你要在某种情况下反对例外,你必须解释如何将条件的范围划分为“例外”和“非例外”。

在某些方面,它类似于回答问题,“程序之间的界限在哪里?”答案是,“无论你把开始和结束放在哪里”,然后我们都可以谈论经验法则和不同的风格,以确定放置它们的位置。没有严格的规则。

答案 1 :(得分:21)

输入“错误”输入的用户也不例外:这是预期的。

异常不应用于正常控制流程。

在过去,许多作者都说过,例外本身就很昂贵。 Jon Skeet的博客与此相反(并在SO上提到了一些答案),并表示它们并不像报道的那样昂贵(尽管我不提倡在紧密循环中使用它们!)

使用它们的最大原因是“意图陈述”,即如果您看到异常处理块,您会立即看到在正常流程之外处理的异常情况。

答案 2 :(得分:8)

有一个重要的其他原因,而不是已提到的原因:

如果您在异常情况下使用异常 ,则可以在调试器中运行调试器设置“抛出异常时停止”。这非常方便,因为您在导致问题的确切行上插入调试器。使用此功能可以为您节省大量时间

在C#中这是可能的(我全心全意地推荐它),特别是在他们将TryParse方法添加到所有数字类之后。通常,没有一个标准库需要或使用“坏”异常处理。当我接近尚未写入此标准的C#代码库时,我总是最终将其转换为无异常的常规情况,因为stop-om-throw非常有价值。

在firebug javascript调试器中,您也可以执行此操作,前提是您的库不会严重使用异常。

当我编写Java时,这实际上是不可能的,因为很多东西都使用异常来处理非特殊情况,包括很多标准的java库。因此,这种节省时间的功能实际上无法在java中使用。我相信这是由于检查了异常,但我不会开始咆哮它们是如何邪恶的。

答案 3 :(得分:6)

错误和例外 - 什么,何时何地?

异常旨在报告错误,从而使代码更加健壮。要了解何时使用异常,首先必须了解错误是什么,错误是什么。

功能是一个工作单元,故障应视为错误或基于其对功能的影响。在函数 f 中,失败是一个错误,当且仅当它阻止 f 满足其任何被调用者的前提条件,实现任何< strong> f 自己的后置条件,或者重新建立 f f 分担维护责任的不变

有三种错误:

  • 阻止函数满足必须调用的另一个函数的前提条件(例如,参数限制)的条件;
  • 阻止函数建立其自己的后置条件之一的条件(例如,产生有效的返回值是后置条件);和
  • 阻止函数重新建立它负责维护的不变量的条件。这是一种特殊的后置条件,特别适用于成员函数。每个非私有成员函数的基本后置条件是它必须重新建立其类的不变量。

任何其他条件错误,不应报告为错误。

为什么例外说输入验证如此糟糕?

我想这是因为对“输入”有点含糊不清的理解是输入函数字段值,后者应该不是抛出异常,除非它是失败函数的一部分。

答案 4 :(得分:5)

我认为差异取决于特定类别的合同,即

对于用于处理用户输入的代码,以及针对它的防御程序(即清理它),为无效输入抛出异常是错误的 - 这是预期的。

对于旨在处理可能源自用户的已经过清理和验证的输入的代码,如果您发现某些意图被禁止的输入,则抛出异常将是有效的。在这种情况下,调用代码违反了合同,它表示清理和/或调用代码中存在错误。

答案 5 :(得分:4)

  1. 可维护性 - 例外创建 奇怪的代码路径,与GOTO不同。
  2. 易用性(适用于其他课程) - 其他课程可以信任 用户提出的例外情况 输入类是实际错误
  3. 效果 - 在大多数语言中, 例外导致表演和 内存使用惩罚。
  4. 语义 - 单词的含义 确实很重要。糟糕的输入不是 “例外”。

答案 6 :(得分:3)

使用例外时,错误处理代码与导致错误的代码分开。这是异常处理的意图 - 作为异常条件,错误无法在本地处理,因此会向某些更高(和未知)的范围抛出异常。如果不处理,应用程序将在完成任何更多操作之前退出。

如果您曾经做过简单的逻辑操作(例如验证用户输入),那么您将做出非常非常非常错误的事情。

  

输入不是预期的结果   因此是例外。

这句话对我来说并不合适。 UI限制用户输入(例如,使用限制最小/最大值的滑块),您现在可以断言某些条件 - 不需要错误处理。或者,用户可以输入垃圾,您希望这种情况发生并且必须处理它。一个或另一个 - 这里没有任何例外。

  

抛出异常允许我这样做   确切地说明了什么是错误的   StringValueTooLong或或   IntegerValueTooLow或InvalidDateValue   管他呢。为什么要这样考虑   错?

我认为这超越了 - 更接近邪恶。您可以定义抽象的ErrorProvider接口,或返回表示错误的复杂对象而不是简单的代码。如何检索错误报告有很多选项。使用例外因为方便所以错误。我只是在写这一段时觉得很脏。

考虑将异常作为希望抛出。最后一次机会。一个祈祷者。验证用户输入不应导致任何这些条件。

答案 7 :(得分:3)

是否有可能出现一些分歧是由于对“用户输入”的含义缺乏共识?事实上,在你正在编码的那一层。

如果您正在编写GUI用户界面或Web表单处理程序,您可能会期望无效输入,因为它直接来自人类的打字手指。

如果您正在编写MVC应用程序的模型部分,您可能已经设计了一些东西,以便控制器为您清理输入。到模型确实是例外的无效输入,可以这样对待。

如果您在协议级别对服务器进行编码,您可能会合理地期望客户端检查用户输入。同样,这里的无效输入确实是一个例外。这与100%信任客户端(确实非常愚蠢)完全不同 - 但与直接用户输入不同,您预测大多数时间输入都可以。这里的线条模糊不清。发生事件的可能性越大,您就越不希望使用异常来处理它。

答案 8 :(得分:3)

这是一个关于此事的语言pov(观点)。

为什么例外说输入验证如此糟糕?

结论:

  • 例外的定义不够明确,因此有不同的意见。
  • 错误的输入被认为是正常的事情,而不是例外。

想法?

这可能归结为人们对所创建代码的期望。

  • 客户端无法信任
    • 验证已经发生在服务器端。 更强: 每次验证都发生在服务器端。
    • 因为验证发生在服务器端,所以期望在那里完成,并且预期的不是例外,因为它是预期的。

然而,

  • 客户的输入无法信任
  • 客户的输入验证 可以受信任
    • 如果验证可信,则可以预期生成有效输入
    • 现在每个输入都有效
    • 无效输入现在是意外的,异常

例外可以是退出代码的好方法。

要考虑的一件事是你的代码是否处于正确的状态。 我不知道什么会使我的代码处于不正确的状态。 连接自动关闭,剩余的变量被垃圾收集,问题是什么?

答案 9 :(得分:2)

针对非例外情况的异常处理的另一次投票!

  1. 在.NET中,即使没有抛出异常,JIT编译器也不会在某些情况下执行优化。以下文章解释得很好。 http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx http://msmvps.com/blogs/peterritchie/archive/2007/07/12/performance-implications-of-try-catch-finally-part-two.aspx

  2. 当抛出异常时,它会为堆栈跟踪生成一大堆信息,如果您实际上“期待”异常,则可能不需要这些信息,这通常是将字符串转换为int等的情况。

答案 10 :(得分:1)

8年后,我陷入了尝试应用CQS模式的困境。我支持输入验证可以抛出异常,但是增加了约束。如果任何输入失败,你需要抛出一种类型的异常:ValidationException,BrokenRuleException等。不要抛出一堆不同的类型,因为它们无法处理它们。这样,您就可以在一个地方获得所有损坏规则的列表。您创建一个负责执行验证(SRP)的类,并在至少1个规则被破坏时抛出异常。这样,你可以用一个捕获来处理一种情况,并且你知道你很好。无论调用什么代码,您都可以处理该场景。这使得下游的所有代码都更加清晰,因为您知道它处于有效状态或者它不会到达那里。

对我而言,从用户那里获取无效数据并不是您通常所期望的。 (如果每个用户第一次向您发送无效数据,我会再次查看您的UI。)任何阻止您处理真实意图的数据,无论是用户还是来自其他地方,都需要中止处理。如果是单个数据,如果它是用户输入,则抛出ArgumentNullException有什么不同呢?它是类上的字段,表示这是必需的。

当然,你可以先做验证并在每一个“命令”上写相同的样板代码,但我认为这是一个维护噩梦,而不是在顶部的一个地方捕获无效的用户输入,无论如何处理相同的方式。 (更少的代码!)只有当用户提供无效数据时才会出现性能损失,这通常不应该发生(或者你的UI不好)。无论如何,客户端上的任何和所有规则都必须在服务器上重写,因此您可以只编写一次,执行AJAX调用,并且&lt;延迟500 ms将为您节省大量编码时间(只需1个位置即可放置所有验证逻辑)。

此外,虽然您可以使用开箱即用的ASP.NET进行一些简洁的验证,但如果您想在其他UI中重复使用验证逻辑,那么您就无法将其纳入ASP.NET。无论使用何种UI,您最好在下面创建并处理它。 (我的2美分,至少。)

答案 11 :(得分:1)

我使用了两种解决方案的组合: 对于每个验证函数,我传递一个我填写验证状态的记录(错误代码)。 在函数结束时,如果存在验证错误,我抛出一个异常,这样我不会为每个字段抛出异常,但只抛出一次。我还利用抛出异常将停止执行,因为我不希望在数据无效时继续执行。

例如

procedure Validate(var R:TValidationRecord);
begin
  if Field1 is not valid then
  begin
    R.Field1ErrorCode=SomeErrorCode;
    ErrorFlag := True; 
  end; 
  if Field2 is not valid then
  begin
    R.Field2ErrorCode=SomeErrorCode;
    ErrorFlag := True; 
  end;
  if Field3 is not valid then
  begin
    R.Field3ErrorCode=SomeErrorCode;
    ErrorFlag := True; 
  end;

  if ErrorFlag then
    ThrowException
end;

如果仅依赖布尔值,使用我的函数的开发人员应该考虑到这一点:

if not Validate() then
  DoNotContinue();

但是他可能忘了并且只打电话给Validate()(我知道他不应该,但也许他可能)。

所以,在上面的代码中我获得了两个优点: 验证函数中只有一个例外。 2例外,甚至未被捕获,将停止执行,并在测试时出现。

答案 12 :(得分:1)

当我看到为验证错误抛出异常时,我经常看到抛出异常的方法是一次性执行大量验证。 e.g。

public bool isValidDate(string date)
{
    bool retVal = true;
    //check for 4 digit year
    throw new FourDigitYearRequiredException();
    retVal = false;

    //check for leap years
    throw new NoFeb29InANonLeapYearException();
    retVal = false;
    return retVal;
}

随着规则在数月和数年内不断增加,此代码往往非常脆弱且难以维护。我通常更喜欢将我的验证分解为返回bool的较小方法。它可以更容易地调整规则。

public bool isValidDate(string date)
{
    bool retVal = false;
    retVal = doesDateContainAFourDigitYear(date);
    retVal = isDateInALeapYear(date);
    return retVal;
}

public bool isDateInALeapYear(string date){}

public bool doesDateContainAFourDigitYear(string date){}

如前所述,返回包含错误信息的错误结构/对象是一个好主意。最明显的优势是你可以收集它们并立即向用户显示所有错误消息,而不是让它们通过验证来玩Whack-A-Mole。

答案 13 :(得分:1)

异常不应该用于输入验证,因为不仅应该在异常情况下使用异常(因为已经指出错误的用户输入不是这样),而是它们创建了特殊的代码(不是很明显)。 / p>

大多数语言中的异常问题是它们改变了程序流的规则,这在一个真正特殊的情况下很好,在这种情况下,不一定能够确定有效流应该是什么,因此只是抛出一个异常并得到然而,如果您知道流应该是什么,您应该创建该流(在列出的情况下,它将向用户发出一条消息,告诉他们需要重新输入一些信息)。

在我每天工作的应用程序中,例外情况确实被过度使用,即使用户在登录时输入了错误的密码,由于逻辑不是应用程序所需要的,因此您的逻辑也是如此。然而,当一个过程有两个结果中的一个是正确的或不正确的时候,我不认为我们可以说,不正确,无论多么错误,都是例外。

使用此代码时遇到的一个主要问题是尝试遵循代码的逻辑而不深入参与调试器。虽然调试器很棒,但应该可以为用户输入错误密码而不必触发密码时添加逻辑。

保留异常以实现真正卓越的执行,而不仅仅是错误。在我突出显示错误密码的情况下并不例外,但无法联系域服务器可能是!

答案 14 :(得分:1)

通常,库会抛出异常,客户端会捕获它们并使用它们做一些智能操作。对于用户输入,我只是编写验证函数而不是抛出异常。对于类似的事情,例外情况似乎过分。

异常存在性能问题,但在GUI代码中,您通常不必担心它们。那么如果验证需要额外100毫秒才能运行呢?用户不会注意到这一点。

在某些方面,这是一个艰难的要求 - 一方面,您可能不希望整个应用程序崩溃,因为用户在邮政编码文本框中输入了一个额外的数字,而您忘记处理异常。另一方面,“早期失败,难以实现”的方法可以确保快速发现和修复错误,并保护您宝贵的数据库。总的来说,我认为大多数框架都建议您不要使用异常处理进行UI错误检查,而某些(如.NET Windows Forms)提供了很好的方法(ErrorProviders和Validation事件),没有例外。

答案 15 :(得分:0)

使用异常的一个问题是一次只能检测到一个问题。用户修复并重新提交,只是为了找到另一个问题!返回需要解析的问题列表的接口更加友好(尽管它可以包含在异常中)。

答案 16 :(得分:0)

我同意米奇的观点,即“不应将例外情况用于正常控制流程”。我只是想从我记忆中的计算机科学课程中添加它,捕获异常是昂贵的。我从来没有真正尝试过基准测试,但比较一下if / else vs try / catch之间的性能会很有趣。