我应该如何处理无效的用户输入?


12

我考虑这个问题已有一段时间了,我很想征询其他开发人员的意见。

我倾向于具有非常防御性的编程风格。我典型的块或方法如下所示:

T foo(par1, par2, par3, ...)
{
    // Check that all parameters are correct, return undefined (null)
    // or throw exception if this is not the case.

    // Compute and (possibly) return result.
}

另外,在计算过程中,我在取消引用所有指针之前先检查它们。我的想法是,如果存在某些错误,并且某些地方应该出现一些NULL指针,则我的程序应该很好地处理此问题,并且只是拒绝继续进行计算。当然,它可以通过日志或其他机制中的错误消息来通知问题。

换句话说,我的方法是

if all input is OK --> compute result
else               --> do not compute result, notify problem

其他开发人员(其中包括我的一些同事)使用另一种策略。例如,它们不检查指针。他们认为应该为一段代码提供正确的输入,并且如果输入错误,则不应对发生的任何事情负责。另外,如果NULL指针异常使程序崩溃,则在测试过程中更容易发现错误,并且更有可能被修复。

我对这个问题的回答通常是:但是,如果在测试过程中未发现该错误,而该错误已在客户已经使用该产品时出现,该怎么办?bug表现出来的首选方式是什么?它应该是没有执行特定操作但仍可以继续工作的程序,还是崩溃并需要重新启动的程序?

总结

您会建议使用以下两种方法处理错误输入吗?

Inconsistent input --> no action + notification

要么

Inconsistent input --> undefined behaviour or crash

编辑

感谢您的回答和建议。我也很喜欢合同设计。但是,即使我相信写过调用我的方法的代码的人(也许是我自己),仍然可能存在错误,导致输入错误。所以我的方法是永远不要假设方法传递了正确的输入。

另外,我将使用一种机制来捕获问题并进行通知。在开发系统上,它将例如打开一个对话框来通知用户。在生产系统中,它只会将一些信息写入日志。我认为额外的检查不会导致性能问题。我不确定断言是否足够,是否在生产系统中将其关闭:也许生产中会发生某些情况,而这种情况在测试期间没有发生。

无论如何,令我感到非常惊讶的是,许多人采用了相反的方法:他们让应用程序“故意”崩溃,因为他们坚持认为这将使测试过程中更容易发现错误。


始终进行防御性编码。最终,出于性能原因,您可以放置​​一个开关以在发布模式下禁用某些测试。
deadalnix

今天,我修复了与缺少NULL指针检查有关的错误。在应用程序注销期间创建了一些对象,构造函数使用吸气剂访问了其他不再存在的对象。当时不打算创建该对象。它是由于另一个错误而创建的:注销时未停止某些计时器->发送了信号->收件人试图创建对象->查询构造函数并使用了另一个对象-> NULL指针->崩溃)。我真的不希望如此讨厌的情况使我的应用程序崩溃。
乔治

1
维修规则:当您必须失败时,请尽早并发声。
deadalnix

“修复规则:当您必须失败时,请大声尝试并尽快失败。”:我猜所有这些Windows的BSOD都是此规则的应用。:-)
乔治

Answers:


8

你说对了。偏执。不要相信其他代码,即使它是您自己的代码。您忘记了事情,进行了更改,代码不断发展。不要相信外部代码。

上面有一个好处:如果输入无效但程序没有崩溃怎么办?然后,您将在数据库中得到垃圾,并在一行中出错。

当要求输入数字(例如,美元价格或单位数)时,我喜欢输入“ 1e9”并查看代码的作用。这有可能发生。

四十年前,从UCBerkeley获得了计算机科学学士学位后,我们被告知一个好的程序可以处理50%的错误。偏执。


是的,恕我直言,这是偏执狂是一种特征而不是问题的少数情况之一。
乔治

“如果输入无效但程序不会崩溃该怎么办?那么您将在数据库中得到垃圾,并在一行中出错。”:如果不是崩溃,程序可能会拒绝执行该操作并返回未定义的结果。未定义将通过计算传播,并且不会产生垃圾。但是程序无需崩溃即可实现这一目标。
乔治

是的,但是-我的意思是程序必须检测无效的输入并加以处理。如果未选中输入,它将可以直接进入系统,然后出现讨厌的情况。甚至崩溃比那更好!
安迪·坎菲尔德

我完全同意您的看法:我的典型方法或函数从一系列检查开始,以确保输入数据正确。
乔治

今天,我再次确认“检查所有内容,不信任任何内容”策略通常是一个好主意。我的一个同事由于缺少检查而出现了NULL指针异常。事实证明,在这种情况下,使用NULL指针是正确的,因为尚未加载某些数据,并且检查指针并且在它为NULL时不执行任何操作是正确的。:-)
乔治

7

您已经有了正确的主意

您会建议使用以下两种方法处理错误输入吗?

输入不一致->无操作+通知

或更好

输入不一致->处理正确的动作

您不能真正采用千篇一律的方法进行编程(但是可以),但是最终会采用公式化设计,这种设计是出于习惯而不是出于有意识的选择。

脾气教条与实用主义。

史蒂夫·麦康奈尔说最好

史蒂夫·麦康奈尔(Steve McConnell)几乎写了关于防御性编程的书(《代码完成》),这是他建议您应始终验证输入内容的方法之一。

我不记得史蒂夫(Steve)是否提到过这一点,但是您应该考虑对非私有方法和函数以及仅在必要时才进行其他处理。


2
我建议不要使用公开的方法,而应使用所有非私有的方法来防御性地涵盖具有保护,共享或没有访问限制概念的语言(所有内容都是公开的,都暗含)。
JustinC 2011年

3

这里没有“正确”的答案,尤其是在没有指定语言,代码类型以及代码可能涉及的产品类型的情况下。考虑:

  • 语言很重要。在Objective-C中,通常可以将消息发送为nil。什么也没发生,但是程序也不会崩溃。Java没有显式指针,因此nil指针并不是这里的大问题。在C语言中,您需要多加注意。

  • 偏执是指无理,毫无根据的怀疑或不信任。对于软件而言,这可能对人没有什么好处。

  • 您的关注程度应与代码中的风险程度以及确定出现的任何问题的可能难度相称。在最坏的情况下会发生什么?用户重新启动程序,并从中断处继续吗?公司损失了数百万美元?

  • 您不能总是识别出错误的输入。您可以将指针宗教比较为零,但只抓住一个出2 ^ 32个可能的值,几乎所有这些都是不好的。

  • 处理错误的机制有很多。同样,它在某种程度上取决于语言。您可以使用断言宏,条件语句,单元测试,异常处理,精心设计和其他技术。它们都不是万无一失的,也不适合每种情况。

因此,它主要归结为您要承担责任的地方。如果要编写供他人使用的库,则可能希望对所获得的输入尽可能地谨慎,并尽可能地发出有用的错误。在您自己的私有函数和方法中,您可能会使用断言来捕获愚蠢的错误,但否则会给调用者(就是您)带来责任,以免传递垃圾。


+1-好答案。我主要担心的是,错误的输入可能会导致生产系统上出现问题(当为时已晚时,不能对此采取任何措施)。当然,我认为您完全正确,这取决于此问题可能对用户造成的损害。
乔治

语言扮演着重要角色。在PHP中,一半的方法代码最终会检查变量的类型并采取适当的措施。在Java中,如果该方法接受一个int,则您将无法再传递其他任何内容,因此您的方法将变得更加清晰。
第一章

1

肯定应该有一个通知,例如抛出异常。它可以帮助其他编码人员,他们可能会滥用您编写的代码(试图将其用于本来不是想做的事情),即他们的输入无效或导致错误。这对于跟踪错误非常有用,而如果您仅返回null,则他们的代码将继续进行,直到他们尝试使用结果并从其他代码获取异常为止。

如果您的代码在调用某些其他代码(可能是数据库更新失败)时遇到错误,而该错误超出了该特定代码的范围,则您实际上无法控制它,唯一的办法是抛出一个异常来解释您知道(只有您所调用的代码告诉您)。如果您知道某些输入将不可避免地导致这种结果,那么您就不必费心执行代码并抛出异常,指出哪个输入无效以及为什么。

关于更多与最终用户相关的注释,最好返回描述性但简单的内容,以便任何人都可以理解。如果您的客户呼叫并说“程序崩溃,请修复它”,那么您将需要进行大量工作来查找问题所在和原因,并希望您可以重现该问题。使用适当的异常处理不仅可以防止崩溃,还可以提供有价值的信息。一位客户打来的电话,说“程序给我一个错误。它说'XYZ不是方法M的有效输入,因为Z太大了”,或类似的东西,即使他们不知道这意味着什么,您确切地知道在哪里看。此外,根据您/您公司的业务实践,可能甚至无法解决这些问题,因此最好给它们留个好图。

因此,我的答案的简短版本是您的第一选择是最好的。

Inconsistent input -> no action + notify caller

1

在上大学编程课程时,我遇到了同样的问题。我倾向于偏执狂,倾向于检查所有事情,但被告知这是误导行为。

我们被教导“按合同设计”。重点是在注释和设计文档中指定前提条件,不变式和后置条件。作为实现我的代码部分的人员,我应该信任软件架构师,并通过遵循包含前提条件的规范来赋予它们权力(我的方法必须能够处理哪些输入以及我将不发送哪些输入) 。在每个方法调用中检查过多会导致膨胀。

在构建迭代期间应使用断言来验证程序的正确性(前提条件,不变式,后置条件的验证)。然后在生产编译中将断言。


0

使用“断言”是通知伙伴开发人员他们做错了的方法,当然,这仅是“私有”方法。启用/禁用它们只是在编译时添加/删除的一个标志,因此很容易从生产代码中删除断言。还有一个很棒的工具可以知道是否在自己的方法中以某种方式做错了。

至于在公共/受保护方法中验证输入参数,我更喜欢进行防御性工作并检查参数并抛出InvalidArgumentException等。这就是为什么有这里。它还取决于您是否正在编写API。如果它是一个API,并且如果它是封闭源,则更多,请更好地验证所有内容,以便开发人员确切地知道出了什么问题。否则,如果源可供其他开发人员使用,则它不是黑白的。只要与您的选择保持一致即可。

编辑:仅添加一下,如果您看一下Oracle JDK,您将看到他们从不检查“ null”并让代码崩溃。由于无论如何都会抛出NullPointerException,为什么还要麻烦检查null并抛出显式异常。我想这是有道理的。


在Java中,您会得到一个空指针异常。在C ++中,空指针会使应用程序崩溃。也许还有其他示例:被零除,索引超出范围等等。
乔治
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.