为啥说异常对输入验证如此不利?

Posted

技术标签:

【中文标题】为啥说异常对输入验证如此不利?【英文标题】:Why are Exceptions said to be so bad for Input Validation?为什么说异常对输入验证如此不利? 【发布时间】:2010-09-29 11:04:14 【问题描述】:

我知道“例外是针对例外情况”[a],但除了再次重复 over 和 over 之外,我从未找到这个事实的真正原因。

由于它们会停止执行,因此您不希望它们用于简单的条件逻辑是有道理的,但为什么不输入验证呢?

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

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

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

另外,我found this article by Martin Fowler 是关于如何处理这些事情的——通知模式。我不确定我如何将其视为不停止执行的异常以外的任何内容。

a:我在任何地方都阅读过有关异常的任何内容。

--- 编辑---

已经提出了许多重要的观点。我评论了大部分内容并为其中的优点 + 了,但我还没有完全相信。

我并不是要提倡将异常作为解决输入验证的正确方法,但我想找到充分的理由说明为什么这种做法被认为如此邪恶,因为大多数替代解决方案似乎只是伪装的异常。

【问题讨论】:

我同意。我想将输入验证和错误消息的显示分开。显示相同的错误消息可以处理无效输入和内部错误情况。 【参考方案1】:

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

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

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

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

【讨论】:

谢谢先生。您帮助定义了我对这些其他很好的答案的挫败感。我一直得到同样的! +1:同意!在我看来,“异常”(名词)不是异常(形容词)的缩写,它就是它所说的,一个异常。如“不符合一般规则的实例或案例”。我想你可能有一个例外。 ;) 值得一提的是,Martin Fowler 在 Refactoring 中提出了基于异常的流控制作为一种理想的重构模式 我认为“异常情况”的一个例子是:每当违反域实体的业务规则时。这里的例外是提醒程序员在 c(r)ud-ing 任何实体之前进行检查。经验法则可能是“域实体永远不应处于无效状态”【参考方案2】:

用户输入“错误”输入也不例外:这是意料之中的。

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

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

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

【讨论】:

好文章,Mitch (yoda.arachsys.com/csharp/exceptions.html),谢谢你 - 虽然这不是很有说服力。 对不起,我会更进一步。 “预期”是一个很难提出的论点。所有的例外都是意料之中的。否则我们无法编写它们。我同意(并声明)控制流)。使用异常进行输入验证难道不包括意图状态吗? @enobrev:您似乎误解了什么是异常。这是在正常操作条件下不应该发生的事情。 @mitch:不幸的是,这只是提出了一个问题,“什么是正常的操作条件?”由于几乎每个人都有不同的定义,因此很难定义例外。 @Sailing Judo:这取决于上下文。一个应用程序无法打开该应用程序所拥有且预期可以打开的所需文件是一个例外。【参考方案3】:

除了已经提到的原因之外,还有一个重要的原因:

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

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

在 firebug javascript 调试器中,您也可以这样做,前提是您的库没有严重使用异常。

当我编写 Java 时,这实际上是不可能的,因为很多事情都在非异常情况下使用异常,包括很多标准 Java 库。所以这个省时的特性在java中并不能真正使用。我相信这是由于检查异常造成的,但我不会开始抱怨它们是多么邪恶。

【讨论】:

好点,克罗森沃尔德。实际上,我想我是希望听到关于邪恶例外的咆哮。我并不完全相信他们是多么邪恶。 嗯,它们是我无法在 java 中使用“stop on throw”的原因。邪恶本身,因为它是一件好事。 调试器上非常有趣的一点在每次抛出异常时都会停止(除非我错了,它在 VS 2003 中被称为第一次机会异常)......我想知道调试器中是否有办法询问它忽略某些类型的异常,并停止在其他类型... +1 ... @paercebal Eclipse 允许您指定将捕获哪些异常类。它默认为 Exception(通配符匹配所有异常)。在调试透视图中,单击 Run -> Add Java Exception Breakpoint...【参考方案4】:

错误和异常 - 什么、何时、何地?

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

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

有三种错误:

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

任何其他情况不是错误,不应报告为错误。

为什么说异常对输入验证如此不利?

我猜这是因为对“输入”的理解有些模棱两可,要么是函数的输入,要么是字段的值,而后者不应该抛出异常,除非它是失败函数的一部分。

【讨论】:

【参考方案5】:
    可维护性 - 创建异常 奇怪的代码路径,与 GOTO 不同。 易于使用(对于其他类)- 其他类可以相信 从您的用户引发的异常 输入类是实际错误 性能 - 在大多数语言中, 异常会导致性能和 内存使用损失。 语义 - 词的含义 确实很重要。输入错误不是 “特殊”。

【讨论】:

我真正的意思是“不明显”,因为对代码的新人不一定能预料到代码的去向。 似乎很难定义输入验证的明显程度。在年龄字段中接收“香蕉”似乎并不明显。一个循环可以被认为是一个 goto,但我们知道它的去向。我明白你关于代码路径的意思,但它们是故意的。 我不敢恭维。其他人也说过类似的话。我绝对认为异常是一个很好的工具,但对于更苛刻的观点,请查看:joelonsoftware.com/items/2003/10/13.html 人脑的性能肯定比最慢的异常实现低很多;在谈论输入异常时,性能参数是稻草人。使用错误代码的类不容易使用;它们只是更容易忽略错误(包括输入错误)。 “奇怪的代码路径”,这应该是什么意思?用过异常的人都知道,异常有一个明确定义的代码路径:UP。 “错误输入并非例外”是主观的,有人可能会争辩说,理想情况下,用户不应该输入错误输入,因此错误输入是例外的。【参考方案6】:

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

对于旨在处理用户输入并为其进行防御性编程(即清理它)的代码,为无效输入抛出异常是错误的 - 这是预期的。

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

【讨论】:

我明白你在说什么,这在符合我所读到的关于异常的一般情况下是有道理的,但是如何使用另一种方法更好地捕获和解决无效输入或比使用异常更“正确”? 我认为是因为这样做是正常程序流程的一部分。通常异常允许您通过仅针对正常业务案例进行编码并忽略(字面上)通常不应该发生的异常情况(例如运行时问题,如磁盘空间不足、内存不足)来保持代码流的整洁。 但是如果用户输入不正确,您最终将停止所有执行并要求用户解决问题,这听起来很像一个例外。 @enobrev:在“友好”的客户端代码中,无效的用户输入不仅会停止一切并说“有问题”,而是会描述特定问题并帮助用户修复它(例如通过突出显示无效字段)。处理特定问题的代码应位于可能发生问题的代码附近(建议错误检查比异常处理更合适)。在应该以相同方式处理许多问题的情况下(例如,接收来自明显错误的客户端代码的提交),异常处理更合适。【参考方案7】:

使用异常时,错误处理代码与导致错误的代码分开。这是异常处理的意图——作为一种异常情况,错误不能在本地处理,因此异常被抛出到更高(和未知)的范围。如果不处理,应用程序将在完成任何其他操作之前退出。

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

输入不是预期的,并且 因此是例外。

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

抛出异常允许我 准确定义问题所在 StringValueTooLong 或 或 IntegerValueTooLow 或 InvalidDateValue 管他呢。为什么考虑这个 错了吗?

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

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

【讨论】:

罚款丹尼尔,谢谢。 “无法在本地处理错误” - 这不是输入验证的一部分吗?输入有问题,现在您必须要求用户解决问题。您正在捕获异常并要求用户解决它,然后才能继续。 “UI 限制了用户输入...或者,用户可以输入垃圾内容,而您希望这会发生” 这并不总是可能的 - 例如在网络实例中,输入必须最终由业务逻辑处理。 “ErrorProvider”最终会冒泡到用户界面进行解析,这不与您的视图或显示逻辑捕获的异常相同吗? @enobrev:先说两点——为什么在这种情况下必须使用异常?例外是一把大枪,只应在别无选择的情况下使用。 @enobrev:到第 3 次,除了异常改变控制流的方式之外,其他类似 - 它们是突然的并停止执行。当抛出异常时,调用者必须非常小心地让应用程序处于正常状态。这是令人讨厌的,讨厌的东西,大多数程序员都没有给予足够的考虑。【参考方案8】:

是否存在一些分歧是由于对“用户输入”的含义缺乏共识?事实上,你在哪一层编码。

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

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

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

【讨论】:

好点,但如果我们谈论的是最瘦的客户端——比如用户在带有 javascript 验证的网页上输入。为什么在检查用户输入的值时使用异常是错误的?其他方法(事件、错误代码、自定义错误类等)如何更正确? 我确实考虑过提到 Javascript 表单验证,但觉得它混淆了重点。我想说的是,在“总是”到“从不”使用异常的范围内,Javascript 验证让你远离“从不” ... 但这意味着脚本小子很容易导致您的 Web 应用程序到处抛出异常。除非用户经过身份验证并信任其行为。这么多因素! 好吧,如果您在调用验证例程的任何代码中明确捕获这些异常,那么这甚至不应该是事后的想法。 关键是人们声称抛出异常是昂贵的。这将是潜在的拒绝服务攻击。【参考方案9】:

这是对此事的语言观点。

为什么说异常对输入验证如此不利?

结论:

异常定义不够清楚,所以意见不一。 错误的输入被视为正常现象,而不是例外。

想法?

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

客户端不能被信任 验证必须发生在服务器端。 更强: 每个验证都发生在服务器端。 因为验证发生在服务器端,所以预计会在服务器端完成,而且预期不是异常,因为这是预期的。

然而,

客户的输入不可信 客户端的输入验证 可以信任 如果验证是可信的,它可以预期产生有效的输入 现在每个输入都应该是有效的 无效输入现在是意外的,一个异常

.

异常是退出代码的好方法。

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

【讨论】:

【参考方案10】:

又一票反对对非异常进行异常处理!

    在 .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

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

【讨论】:

与性能相关的优点,似乎因语言而异。在管理输入的情况下,性能似乎并不总是像输入提交后可能发生的事情那样优先。【参考方案11】:

一般来说,库会抛出异常,客户端会捕获它们并对其进行智能处理。对于用户输入,我只编写验证函数而不是抛出异常。像这样的例外似乎过多。

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

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

【讨论】:

我是一名 Windows 窗体程序员,所以我使用错误提供程序和验证事件来处理所有事情。在其他语言中,我会寻找类似的功能,或者有类似验证函数的东西,它们返回 bool 值并将参数用于描述性消息。 谢谢 Dana,现在我可能错了(我必须同时查找),但在我看来,验证事件是一个非停止异常,而 ErrorProvider 是该事件的扩展一直导致在表单字段附近显示错误。我不确定我是否看到了区别。 验证事件只是事件,如按钮点击等。他们一点也不例外。事件不继承自 System.Exception。 我明白这一点,但我不确定我是否看到了功能上的差异。毕竟,任何语言中的异常只是一个从暂停方法返回的对象。除了停止之外,这与事件对象有何不同? 事件只是调用方法的一种方式。异常会导致运行时跳出正常的控制流,以便准备好终止它,从而影响性能。【参考方案12】:

不应将异常用于输入验证,因为不仅应在异常情况下使用异常(正如已指出的那样,不正确的用户输入不是),而且它们会创建异常代码(不是在出色的意义上)。

大多数语言中异常的问题是它们改变了程序流的规则,这在真正的例外情况下很好,因为不一定能确定我们的有效流应该是什么,因此只需抛出异常并获取但是,在您知道流程应该是什么的地方,您应该创建该流程(在列出的情况下,它将向用户发出一条消息,告诉他们他们需要重新输入一些信息)。

在我每天工作的应用程序中确实过度使用了异常,甚至在用户在登录时输入了错误密码的情况下,根据您的逻辑,这将是异常结果,因为它不是应用程序想要的。然而,当一个过程有两种结果之一时,无论是正确的还是不正确的,我认为我们不能说不正确,无论多么错误,都是例外的。

我在使用此代码时发现的一个主要问题是试图遵循代码的逻辑而不深入参与调试器。尽管调试器很棒,但应该可以为用户输入错误密码时发生的情况添加逻辑,而无需启动它。

为真正的异常执行保留异常,而不仅仅是错误。在我强调密码错误的情况下并不例外,但可能无法联系到域服务器!

【讨论】:

我理解并感受到您对设计不佳的应用程序的痛苦。但我敢打赌,令人头疼的问题不仅仅是抛出异常进行验证,因为输入验证往往只是设计良好的应用程序的一小部分。 例程同时拥有“Do”和“Try”版本通常很有帮助,如果“Do”版本失败则抛出异常。这意味着如果操作失败,具有后备位置的代码可以处理此类失败而不会引发异常,而没有后备位置的代码可以在没有显式错误处理的情况下快速失败。【参考方案13】:

当我看到因验证错误而引发异常时,我经常看到引发异常的方法同时执行大量验证。例如

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;

随着规则在数月和数年内堆积如山,此代码往往非常脆弱且难以维护。我通常更喜欢将我的验证分解为返回布尔值的较小方法。它使调整规则变得更加容易。

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。

【讨论】:

我理解并同意您关于将所有验证集中在一个地方的观点,但例外与“返回包含错误信息的错误结构/对象”不同吗? 基本上是的,只是系统对异常的处理方式不同。异常停止/中断您的代码流,您必须停止并处理异常。【参考方案14】:

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

例如

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()(我知道他不应该,但也许他可能)。

所以,在上面的代码中,我获得了两个优点: 1-验证函数中只有一个异常。 2-异常,即使未被捕获,也会停止执行,并在测试时出现。

【讨论】:

【参考方案15】:

8 年后,我在尝试应用 CQS 模式时遇到了同样的困境。我站在输入验证可以抛出异常的一边,但有一个额外的约束。如果任何输入失败,您需要抛出一种类型的异常:ValidationException、BrokenRuleException 等。不要抛出一堆不同的类型,因为不可能全部处理。这样,您就可以在一个地方获得所有违反规则的列表。您创建一个负责执行验证 (SRP) 的类,如果至少有 1 条规则被破坏,则抛出异常。这样一来,您就可以一次处理一种情况,并且您知道自己很好。无论调用什么代码,您都可以处理这种情况。这使下游的所有代码更加清晰,因为您知道它处于有效状态,否则它不会到达那里。

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

当然,您可以先进行验证,然后在每个“命令”上编写相同的样板代码,但我认为这是一场维护噩梦,而不是在顶部的一个地方全部捕获无效的用户输入,无论如何都以相同的方式处理. (更少的代码!)只有当用户提供无效数据时,性能才会受到影响,这种情况不应该经常发生(或者你的 UI 很糟糕)。无论如何,客户端的所有规则都必须在服务器上重新编写,因此您只需编写一次,进行 AJAX 调用,

此外,虽然您可以使用开箱即用的 ASP.NET 进行一些简洁的验证,但如果您想在其他 UI 中重复使用您的验证逻辑,则不能,因为它已融入 ASP.NET。无论使用什么 UI,你最好在下面创建一些东西并在上面处理它。 (至少我的 2 美分。)

【讨论】:

【参考方案16】:

我同意 Mitch 的观点,即“异常不应用于正常的控制流”。我只想从我在计算机科学课上的记忆中补充一点,捕获异常是昂贵的。我从来没有真正尝试过进行基准测试,但是比较 if/else 与 try/catch 之间的性能会很有趣。

【讨论】:

感谢 Barneytron 的回复,尽管 Mitch 对这篇文章提出了很好的观点,但另有说明:yoda.arachsys.com/csharp/exceptions.html 哇,感谢您提供的链接 enobrev。看起来米奇在原始回复中添加了更多信息。真是好东西!【参考方案17】:

使用异常的一个问题是倾向于一次只检测一个问题。用户修复并重新提交,却发现另一个问题!返回需要解决的问题列表的接口更加友好(尽管它可以包装在异常中)。

【讨论】:

当然,但您可能会捕获并收集异常。 更好的是在您键入时检测错误的输入(在网络上这将需要 javascript)。到用户提交时,输入总是干净的,然后服务器必须仅出于安全目的进行验证。在这种情况下,例外情况会很好。

以上是关于为啥说异常对输入验证如此不利?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 vuelidate 验证器不再对输入更改做出反应?

为啥 Facebook 身份验证 SDK 在不同模式下的表现如此不同?

为啥输入验证码时明明输入正确 却显示错误

为啥drawRect的空实现:会对动画期间的性能产生不利影响

为啥总说 对不起,您输入的验证码错误,请重新输入后再提交! 明明是对的呀

为啥使用suphp类型之后网站后台登陆老是提示验证错误呢?