为什么Exceptions对于inputvalidation是如此糟糕?
我明白,“例外情况是例外”,但除了一遍又一遍的重复,我从来没有find这个事实的真正原因。
因为他们停止执行,所以你不会希望他们使用简单的条件逻辑,但为什么不inputvalidation呢?
假设您要循环input一组input,并捕获每个exception,将它们组合在一起以供用户通知…我总是看到这是某种“错误的”,因为用户始终input错误的input,但是这一点似乎是基于关于语义 。
投入是不是预期的,因此是例外。 抛出exception允许我准确定义StringValueTooLong或IntegerValueTooLow或InvalidDateValue或其他什么错误。 为什么这被认为是错误的?
抛出exception的替代方法是返回(并最终收集)错误代码,或者更糟的是错误string。 然后,我会直接显示这些错误string,或parsing错误代码,然后显示相应的错误消息给用户。 不会将例外视为可延展的错误代码吗? 为什么要创build一个单独的错误代码和消息表,当这些错误代码和消息可以用我的语言已经内置的exceptionfunction进行概括时?
另外,我发现Martin Fowler的这篇文章是关于如何处理这种事情的 – 通知模式。 我不知道我是如何看到这是除了不停止执行的例外。
a:到处都是关于例外的东西。
—编辑—
许多伟大的观点已经提出。 我已经评论了大多数,而且还有很多好处,但我还没有完全确信。
我并不是要主张exception作为解决inputvalidation的正确方法,但是我想find很好的理由,为什么这种做法被认为是如此邪恶,因为似乎大多数替代解决scheme只是变相的例外。
阅读这些答案,我发现说“例外只能用于例外条件”是非常没有帮助的。 这引出了什么是“特殊情况”的整个问题。 这是一个主观的术语,其最好的定义是“任何你的正常逻辑stream程无法处理的条件”。 换句话说,一个特殊的条件是你处理使用exception的任何条件。
我很好,作为一个定义,我不知道我们会得到比这更接近。 但是你应该知道这就是你正在使用的定义。
如果你打算在某种情况下反对例外情况,你必须解释如何将整个条件分成“特殊”和“非特例”。
在某些方面,这与回答“过程之间的界限在哪里”的问题类似。 答案是:“无论你把开始还是结束放在哪里”,然后我们可以谈论经验法则和不同的风格,以确定把它们放在哪里。 没有硬性规定。
input“坏”input的用户不是一个例外:这是可以预料的。
例外不应该用于正常的控制stream程。
过去许多作者都说exception本质上是昂贵的。 乔恩·斯基特(Jon Skeet)反驳说这个(在这里提到了几个回答),说他们没有报道的那么贵(尽pipe我不会主张在一个紧密的循环中使用它们)。
使用它们的最大理由是“意向声明”,即如果你看到一个exception处理块,你马上就会看到正常stream程之外处理的exception情况。
除了已经提到的其他原因之外,还有一个重要的原因:
如果仅在例外情况下使用exception,则可以在debugging器中运行debugging器设置“exception抛出时停止”。 这是非常方便的,因为你在导致问题的确切线上进入debugging器。 使用此function可以为您节省大量的时间。
在C#中这是可能的(我完全推荐它),特别是在将TryParse方法添加到所有数字类之后。 一般来说,标准库都不需要或使用“坏”exception处理。 当我接触到一个没有写入这个标准的C#代码库时,我总是把它转换为常规的例外情况,因为stop-om-throw是非常有价值的。
在萤火虫JavaScript的debugging器,你也可以这样做,只要你的图书馆不使用exception严重。
当我编程Java时,这是不可能的,因为很多事情在非例外的情况下使用exception,包括很多标准的Java库。 所以这个节省时间的function在java中是不可用的。 我相信这是由于检查exception,但我不会开始咆哮他们如何是邪恶的。
错误和例外 – 什么,何时何地?
例外是为了报告错误,从而使代码更健壮。 要理解何时使用exception,首先必须了解什么是错误,哪些不是错误。
function是一个工作单元,失败应该被视为错误,或者基于它们对function的影响。 在函数f中 ,失败是一个错误,当且仅当它阻止f满足其被调用者的任何先决条件 ,实现f的任何后置条件 ,或者重新build立f个共享维护责任的不variables 。
有三种错误:
- 阻止函数满足必须调用的另一个函数的前提条件(例如,参数限制)的条件;
- 阻止函数build立其自身后置条件之一的条件(例如,产生有效的返回值是后置条件); 和
- 一个条件,防止函数重新build立它负责维护的不variables。 这是一种特别适用于成员函数的特殊后置条件。 每个非私人成员函数的基本后置条件是它必须重新build立它的类的不variables。
其他任何情况都不是错误,不应该作为错误报告。
为什么Exceptions对于inputvalidation是如此糟糕?
我想这是因为对“input”的理解有些模棱两可,认为input是一个函数的意义input,或者是一个字段的值,后者除非是失败函数的一部分,否则不应抛出exception。
我认为差异取决于特定类别的合同,即
对于用来处理用户input的代码,以及为其进行防御性编程(即对其进行消毒),为无效input引发exception将是错误的 – 这是预期的。
对于代码来处理已经过清理和validation的input,这些input可能源于用户,如果您发现一些意图被禁止的input,抛出exception将是有效的。 在这种情况下,调用代码违反了合同,并且指示了消毒和/或调用代码中的错误。
- 可维护性 – exception会创build奇怪的代码path,与GOTO不同。
- 易于使用 (对于其他类) – 其他类可以相信从您的用户input类引发的exception是实际的错误
- 性能 – 在大多数语言中,exception会导致性能和内存使用损失。
- 语义 – 词的意义是重要的。 不好的input不是“特殊的”。
有些分歧是否可能是由于对“用户input”的含义缺乏共识? 事实上,你在编码的层面。
如果您正在编写GUI用户界面或Web表单处理程序,那么您可能会期望得到无效的input,因为它直接来自人的打字手指。
如果您正在编写MVC应用程序的模型部分,您可能已经devise了一些东西,以便控制器为您清理input。 无效input模型确实是一个例外,并可能被视为例外。
如果您在协议级别编码服务器,您可能会合理地期望客户端正在检查用户input。 再次,这里的无效input确实是一个例外。 这与100%信任客户(这确实是非常愚蠢的)完全不同 – 但是与直接用户input不同,您预测大部分时间input都是可以的。 有些线条模糊。 事情发生的可能性越大,您希望使用exception来处理它的可能性就越小。
这是一个关于这个问题的语言学观点。
为什么Exceptions对于inputvalidation是如此糟糕?
结论:
- 例外情况没有明确的定义,所以有不同的意见。
- 错误的input被认为是正常的事情,而不是一个例外。
想法?
这可能归结于对所创build代码的期望。
- 客户端不能被信任
- validation必须发生在服务器端。 更强: 每个validation发生在服务器端。
- 因为validation发生在服务器端,所以预计在那里完成,预期的并不是一个例外,因为它是预期的。
然而,
- 客户的input不能被信任
- 客户端的inputvalidation 可以被信任
- 如果validation是可信的,则可以期望产生有效的input
- 现在每个input都是有效的
- 无效的input现在是意外的,一个例外
。
exception可以是退出代码的一个好方法。
提到要考虑的事情是如果你的代码保持在适当的状态。 我不知道什么会使我的代码处于不正确的状态。 连接会自动closures,剩下的variables会被垃圾收集,有什么问题?
使用exception时, error handling代码与导致错误的代码分离 。 这是exception处理的意图 – 作为一个例外情况,错误不能在本地处理,所以抛出一个exception被抛到一些更高(和未知)的范围。 如果没有处理,应用程序将退出,然后再努力完成。
如果你曾经在做简单的逻辑操作时抛出exception,比如validation用户input,那么你正在做的事情非常非常非常错误。
投入是不是预期的,因此是例外。
这个说法跟我不一样。 用户界面可以限制用户input(例如,使用限制最小/最大值的滑块),现在可以声明某些条件 – 不需要任何error handling。 或者,用户可以input垃圾,你期望这种情况发生,必须处理。 一个或另一个 – 这里没有任何例外。
抛出exception允许我准确定义StringValueTooLong或IntegerValueTooLow或InvalidDateValue或其他什么错误。 为什么这被认为是错误的?
我认为这更接近邪恶。 你可以定义一个抽象的ErrorProvider接口,或者返回一个表示错误的复杂对象而不是简单的代码。 关于如何检索错误报告,有许多选项。 因为使用exception很方便 ,所以错了 。 我只是写这段文字而感到肮脏。
想想抛出一个例外,如希望。 最后的机会 一个祈祷者。 validation用户input不应导致任何这些情况。
另一个投票反对exception处理的东西,不是例外!
-
在.NET中,即使不引发exception,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
-
当一个exception被抛出时,它会为堆栈跟踪生成一大堆信息,如果实际上“期待”这个exception,那么在将string转换为int等情况下通常会出现这种情况。
一般来说,图书馆会抛出exception,客户端会抓住他们,并对他们做出明智的做法 对于用户input,我只是写validationfunction,而不是抛出exception。 例外情况似乎过于这样的事情。
有例外的性能问题,但在GUI代码中,您通常不必担心它们。 那么如果validation需要额外的100毫秒运行呢? 用户不会注意到这一点。
在某些方面,这是一个艰难的通话 – 一方面,你可能不希望让你的整个应用程序崩溃,因为用户在一个邮政编码文本框中input一个额外的数字,你忘了处理exception。 另一方面,“尽早失败,难以实现”的方法可以确保快速发现和修复错误,并保持宝贵的数据库健全。 一般来说,我认为大多数框架都build议您不要使用exception处理来进行UI错误检查,而像.NET Windows Forms这样的一些提供了很好的方法来执行此操作(ErrorProviders和Validation事件),没有例外。
例外情况不应该用于inputvalidation,因为不仅在特殊情况下应该使用例外情况(因为它被指出不正确的用户input不是),而且它们会创build特殊的代码(不是聪明的意思)。
大多数语言的exception问题是他们改变了程序stream程的规则,这在一个非常特殊的情况下是很好的,在这种情况下,我们不一定能够确定有效的stream程应该是什么,因此只是抛出一个exception,你知道stream程应该是什么,你应该创build该stream程(在列出的情况下,将向用户发出一条消息,告诉他们需要重新input一些信息)。
例外在我每天工作的应用程序中真正过度使用,甚至在用户login时input错误密码的情况下,您的逻辑将会是exception结果,因为它不是应用程序想要的。 然而,当一个过程有两个结果的其中一个正确或不正确的时候,我不认为我们可以这样说,不正确的,不pipe多么错误,都是例外。
我在使用这个代码时发现的一个主要问题就是试图遵循代码的逻辑,而没有深入debugging器。 虽然debugging器非常好,但应该可以添加逻辑来解决用户在input不正确密码的情况下发生的情况,而无需启动。
保持例外,真正的例外执行不仅仅是错误的。 在这种情况下,我突出了让你的密码错误是不是例外,但不能联系域服务器可能!
当我看到exception抛出的validation错误时,我经常看到抛出exception的方法是一次执行大量的validation。 例如
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; }
这些代码往往非常脆弱,难以维护,因为这些规则在几个月和几年之内堆积如山。 我通常更喜欢把我的validation分解成更小的方法来返回布尔值。 这可以更轻松地调整规则。
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){}
正如已经提到的,返回一个错误的结构/对象包含错误的信息是一个好主意。 最明显的好处是你可以收集起来,并一次性向用户显示所有的错误消息,而不是让他们玩validation的重击。
我使用了两个解决scheme的组合:对于每个validationfunction,我传递一个logging,我填写validation状态(错误代码)。 在函数的结尾,如果validation错误存在,我抛出一个exception,这样我不会为每个字段抛出exception,但只有一次。 我也利用抛出一个exception将停止执行,因为我不希望数据无效时继续执行。
例如
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-在validation函数中只有一个例外。 2 – 例外,即使未被捕获,将停止执行,并出现在testing时间。
8年后,我也遇到了同样的困境,试图应用CQS模式。 我认为inputvalidation可以抛出一个exception,但有一个额外的限制。 如果任何input失败,则需要抛出一种exception:ValidationException,BrokenRuleException等。不要抛出一堆不同的types,因为不可能全部处理它们。 这样,你就可以在一个地方得到所有破碎规则的清单。 您创build了一个负责进行validation(SRP)的类,如果至less有一条规则被破坏,则会引发exception。 那样的话,你只需要一个处理就能处理一个情况,而且你知道你很好。 无论调用什么代码,都可以处理该场景。 这使得下游的所有代码更加清洁,因为您知道它处于有效状态,否则就不会到达那里。
对我来说,从用户那里获取无效的数据不是你通常所期望的。 (如果每个用户第一次向你发送无效的数据,我会再看看你的用户界面。)任何阻止你处理真正意图的数据,无论是用户还是其他地方都需要中止处理。 如果它是用户input,它与从一个单一数据中抛出一个ArgumentNullException的方式有什么不同?它是一个类,表示这是必需的。
当然,你可以先进行validation,然后在每一个“命令”上写下相同的样板代码,但是我认为这是一个维护的噩梦,而不是无效地捕获无效的用户input。 (更less的代码!)只有当用户提供了无效的数据时才会出现性能问题,这种情况不应该经常发生(或者你的UI不好)。 客户端的所有规则都必须在服务器上重写,所以你可以只写一次,做一个AJAX调用,<500ms的延迟将为你节省大量的编码时间(只有1放置所有的validation逻辑)。
另外,虽然您可以使用ASP.NET进行一些简洁的validation,但是如果您想在其他UI中重新使用validation逻辑,则无法将其纳入ASP.NET。 无论使用哪个UI,最好在下面创build一些东西并处理它。 (我的2美分,至less)
我同意Mitch的说法:“不应该将exception用于正常控制stream程”。 我只想补充一点,就是从我从计算机科学类中记得的,捕捉exception是很昂贵的。 我从来没有真正尝试过做基准testing,但是比较一下if / else和try / catch之间的性能会是很有趣的。
使用例外的一个问题是一次只能检测到一个问题的倾向。 用户修复并重新提交,只是为了find另一个问题! 返回需要parsing的问题列表的接口非常友好(尽pipe它可能被封装在一个exception中)。