即使我知道该方法无法返回错误的输入,我也应该验证方法调用的返回值吗?


30

我想知道我是否应该通过验证方法调用的返回值是否符合我的期望来进行防御,即使我知道我正在调用的方法将满足此类期望也是如此。

给予

User getUser(Int id)
{
    User temp = new User(id);
    temp.setName("John");

    return temp;
}

我应该做吗

void myMethod()
{
    User user = getUser(1234);
    System.out.println(user.getName());
}

要么

void myMethod()
{
    User user = getUser(1234);

    // Validating
    Preconditions.checkNotNull(user, "User can not be null.");
    Preconditions.checkNotNull(user.getName(), "User's name can not be null.");

    System.out.println(user.getName());
}

我是在概念上问这个问题。如果我知道我正在调用的方法的内部工作原理。是因为我写了它,还是因为检查了它。它返回的可能值的逻辑符合我的先决条件。跳过验证是“更好”还是“更合适”,还是在继续执行我当前正在实现的方法之前,即使它总是可以通过,我仍然应该避免错误的值。


我从所有答案中得出的结论(请随意得出结论):

何时断言
  • 该方法在过去表现出不当行为
  • 该方法来自不受信任的来源
  • 该方法在其他地方使用,没有明确声明其后置条件
在以下情况下不要断言:
  • 该方法与您的方法紧密相关(有关详细信息,请参见选择的答案)
  • 该方法使用适当的文档,类型安全性,单元测试或后置条件检查等明确定义其合同。
  • 性能至关重要(在这种情况下,调试模式声明可以用作混合方法)

1
争取实现%100的代码覆盖率!
Jared Burrows 2015年

9
您为什么只关注一个问题(Null)?如果名称是""(不是null而是空字符串)或,可能也同样有问题"N/A"。您要么可以信任结果,要么必须适当地偏执。
MSalters 2015年

3
在我们的代码库中,我们有一个命名方案:getFoo()承诺不返回null,而getFooIfPresent()可以返回null。
pjc50 2015年

您对代码健全性的假定来自何处?您是否假定该值始终是非null的,是因为它在输入时已经过验证,或者是由于检索该值的结构?而且,更重要的是,您是否正在测试每种可能的极端情况?包括恶意脚本的可能性?
Zibbobz

Answers:


42

这取决于getUser和myMethod更改的可能性,更重要的是,它们彼此独立更改的可能性

如果您以某种方式肯定地知道getUser在将来永远不会改变,那么是的,这是浪费时间进行验证,就像ii = 3;声明之后立即浪费时间进行验证一样(值3)。实际上,您并不知道。但是您知道一些事情:

  • 这两个功能是否“一起生活”?换句话说,他们是否拥有相同的人员来维护它们,它们是否属于同一文件,因此他们是否有可能彼此保持“同步”?在这种情况下,添加验证检查可能是多余的,因为这仅意味着每次两个函数更改或重构为不同数量的函数时,都必须更改(并可能产生错误)更多代码。

  • getUser函数是否是具有特定合同的书面API的一部分,而myMethod仅仅是另一个代码库中的该API的客户端?如果是这样,您可以阅读该文档以了解您是否应该验证返回值(或预先验证输入参数!),或者盲目遵循快乐路径是否真的安全。如果文档中没有明确说明,请维护人员进行修复。

  • 最后,如果该特定函数过去以突然破坏代码的方式突然改变了其行为,那么您将无异于对其偏执。错误倾向于聚集。

请注意,即使您是两个函数的原始作者,上述所有内容均适用。我们不知道这两个功能是否会在它们的余生中“一起生活”,或者它们是否会慢慢分散到单独的模块中,或者您是否以某种方式将自己与老一辈的臭虫打成一片getUser的版本。但是您可能可以做出相当不错的猜测。


2
另外:如果getUser以后要更改,以便它可以返回null,则显式检查是否会为您提供从“ NullPointerException”和行号无法获得的信息?
user253751

您似乎可以验证两件事:(1)该方法正常运行,因为它没有错误。(2)该方法尊重您的合同。我可以看到验证#1是过大的。您应该进行#1测试。这就是为什么我不验证i = 3;的原因。所以我的问题与#2有关。我喜欢您的答案,但同时,这也是您的判断答案。您是否看到显式地验证您的合同,然后又浪费了一些时间写断言而引起任何其他问题?
Didier A.

唯一的“问题”是,如果您对合同的解释错误或变得错误,那么您就必须更改所有验证。但这也适用于所有其他代码。
Ixrec 2015年

1
不能与人群对抗。在回答我的问题主题时,这个答案绝对是最正确的。我想通过阅读所有答案(尤其是@MikeNakis)来补充这一点,我现在认为当您拥有Ixrec的最后两个要点时,至少应该在调试模式下声明前提条件。如果您面临第一个要点,那么断言您的先决条件可能是过大了。最后,给定明确的合同,例如适当的文档,类型安全性,单元测试或后置条件检查,您不应主张先决条件,因为这是过大的。
Didier A.

22

Ixrec的回答很好,但是我将采用不同的方法,因为我认为值得考虑。出于讨论的目的,我将讨论断言,因为这是您Preconditions.checkNotNull()传统上被称为您的名字。

我想建议的是,程序员常常高估了他们确定某种事物将以某种方式表现的程度,而低估了这种行为的后果。另外,程序员通常没有意识到断言的强大功能。随后,以我的经验,程序员断言的频率比他们应有的少。

我的座右铭是:

您应该一直问自己的问题不是“我应该坚持吗?” 但是“我有什么要断言的吗?”

自然,如果您完全确定某件事是以某种方式表现的,那么您将避免断言它确实以这种方式表现,并且在大多数情况下是合理的。如果您不信任任何东西,那么实际上不可能编写软件。

但是,即使是这个规则也有例外。奇怪的是,以Ixrec为例,存在一种情况,非常需要遵循确实在一定范围内int i = 3;的断言i。在这种情况下,i尝试各种假设情景的人很可能会更改此情况,并且随后的代码依赖于i某个值在一定范围内,并且通过快速查看以下代码,并不能立即看出其含义。可接受的范围是。因此,int i = 3; assert i >= 0 && i < 5;乍一看似乎很荒谬,但是如果您考虑一下,它会告诉您可以使用i,并且还告诉您必须停留的范围。断言是最好的文档类型,因为它是由机器强制执行的,因此是一个完美的保证,并且它与代码一起被重构,因此始终保持相关性。

您的getUser(int id)示例与该assign-constant-and-assert-in-range示例之间的关系不是很远。根据定义,您会看到,当事物表现出与我们预期的行为不同的行为时,就会发生错误。没错,我们不能断言所有事情,因此有时会进行一些调试。但是,业内公认的事实是,我们越早发现错误,花费越少。您应该问的问题不仅是如何确保getUser()永远不会返回null(也永远不会返回名称为null的用户),而且,如果其余的代码都将在其中发生什么坏事呢?事实上,有一天会返回意外的结果,并且通过快速查看其余代码以准确了解期望的结果是多么容易getUser()

如果意外行为会导致数据库损坏,那么即使您有101%的把握不会发生,您也应该断言。如果null用户名会引起一些奇怪的神秘的,不可追踪的错误,那么可能会导致数千行下降,并在数十亿个时钟周期之后发生,那么最好是尽早地使失败。

因此,即使我不同意Ixrec的回答,我还是建议您认真考虑以下事实:断言不花任何钱,因此,通过自由地使用断言,实际上并没有什么可以失去的,只有收获。


2
是。断言。我来这里或多或少给出这个答案。++
RubberDuck

2
@SteveJessop我会更正为... && sureness < 1)
Mike Nakis 2015年

2
@MikeNakis:编写单元测试时,这始终是一场噩梦,接口允许的返回值实际上无法从任何输入中生成;-)无论如何,当您有101%的把握时,那么您肯定是错的!
史蒂夫·杰索普

2
+1指出我在答案中完全忘记提及的内容。
Ixrec 2015年

1
@DavidZ是的,我的问题是运行时验证。但是说您在调试中不信任该方法,而在prod中信任它,则可能是该主题的一个好观点。因此,C风格的断言可能是应对这种情况的好方法。
Didier A.

8

绝对没有

调用方永远不要检查它所调用的函数是否遵守其合同。原因很简单:潜在的调用者很多,从概念上讲,对于每个调用者来说,复制检查其他函数结果的逻辑都是不切实际的甚至是错误的。

如果要进行任何检查,则每个功能应检查自己的后置条件。后置条件使您有信心正确实现了该功能,并且用户将能够依赖于您的功能合同。

如果某个功能不打算返回null,则应将其记录在案并成为该功能合同的一部分。这就是这些合同的重点:为用户提供了一些可以依赖的不变式,因此在编写自己的函数时,他们面临的障碍更少。


我同意总体观点,但是在某些情况下,验证返回值肯定是有意义的。例如,如果您调用外部Web服务,则有时您希望至少验证答案的格式(xsd等)
AK_15年

似乎您是在提倡防御设计而不是契约式设计。我认为这是一个很好的答案。如果该方法具有严格的合同,我可以相信。就我而言,它不是,但是我可以更改它来这样做。说我不能吗?您是否认为我应该信任实施?即使它未在文档中明确声明其不返回null或实际上具有后置条件检查?
Didier A.

在我看来,您应该运用最佳判断力,权衡不同方面,例如您对作者的正确行为的信任程度,该函数的界面更改的可能性以及时间限制的严格程度。话虽这么说,大多数时候,函数的作者对函数的合同应该有一个清晰的认识,即使他没有记录它。因此,我说您应该首先信任作者做正确的事情,并且只有在您知道函数编写得不好时才采取防御措施。
保罗

我发现首先信任他人做正确的事可以帮助我更快地完成工作。话虽如此,当问题确实发生时,我编写的应用程序的类型很容易修复。那些使用对安全性要求更高的软件的人可能会认为我太宽容,因此倾向于防御性更高的方法。
保罗

5

如果null是getUser方法的有效返回值,则您的代码应使用If(不是)异常来处理它-这不是特例。

如果null不是getUser方法的有效返回值,则getUser中存在错误-getUser方法应引发异常或以其他方式修复。

如果getUser方法是外部库的一部分,或者无法更改,并且您希望在返回null的情况下引发异常,则包装类和方法以执行检查并引发异常,以便对getUser的调用在您的代码中是一致的。


Null不是getUser的有效返回值。通过检查getUser,我看到实现永远不会返回null。我的问题是,我应该信任检查但不检查我的方法,还是应该怀疑检查并且仍然要检查。方法也有可能在将来更改,从而打破了我不返回null的期望。
Didier A.

如果将getUser或user.getName更改为抛出异常怎么办?您应该进行检查,但不能作为使用用户方法的一部分。如果您担心getUser代码的将来更改会被破坏,请包装getUser方法-甚至是User类-以执行合同。还创建单元测试以检查您对类行为的假设。
MatthewC,2015年

@didibus解释您的答案... 笑脸图释不是getUser的有效返回值。通过检查getUser,我看到该实现永远不会返回一个笑脸图释。我的问题是,我应该信任检查但不检查我的方法,还是应该怀疑检查并且仍然要检查。将来方法也可能会改变,从而超出了我不返回笑脸图释的期望。调用者的职责不是确认被调用的函数正在履行其合同。
文斯·奥沙利文

@ VinceO'Sullivan似乎“合同”是问题的核心。我的问题是隐性合同与显性合同。一个返回int的方法,我不会断言我没有得到一个字符串。但是,如果我只想要正整数,应该吗?这只是隐式的,该方法不会返回负int,因为我检查了它。从签名或文档中尚不清楚它是否不清楚。
Didier A.

1
合同是隐式的还是显式的都没有关系。当给定有效数据时,验证被调用的方法返回有效数据不是调用代码的职责。在您的示例中,您知道否定的init并不是正确的答案,但是您知道肯定的int是否正确?您真正需要多少测试才能证明该方法有效?唯一正确的方法是假设该方法正确。
文斯·奥沙利文

5

我认为您的问题的前提是不正确的:为什么要使用空引用开头?

空对象模式是返回的对象,基本上什么都不做的方式:“没有用户”,而不是返回NULL您的用户,返回表示对象

该用户将处于非活动状态,名称为空,并且其他非空值表示“此处无内容”。主要好处是您不需要乱扔程序,就可以使检查,日志记录等无效,您只需使用对象即可。

为什么算法要关心空值?步骤应相同。拥有一个返回零的空对象,空白字符串等,这同样容易并且更加清晰。

这是一个检查空值的代码示例:

User u = getUser();
String displayName = "";
if (u != null) {
  displayName = u.getName();
}
return displayName;

这是使用空对象的代码示例:

return getUser().getName();

哪个更清楚?我相信第二个是。


虽然该问题被标记为,但值得注意的是,在使用引用时,空对象模式在C ++中确实非常有用。Java引用更像C ++指针,并且允许null。C ++引用无法保存nullptr。如果有人想拥有类似于C ++中的null引用的内容,那么null对象模式是我所知道的唯一实现此目的的方法。


空只是我所断言的例子。我很可能需要一个非空的名称。我的问题仍然适用,您可以验证吗?或者它可能返回int,而我不能有负数int,或者它必须在给定范围内。不要说每个人都认为这是一个空洞的问题。
Didier A.

1
为什么方法会返回不正确的值?应该检查的唯一位置是应用程序的边界:与用户和其他系统的接口。否则,这就是我们有例外的原因。

想象一下我有:int i = getCount; 浮点数x = 100 / i; 如果我知道getCount不返回0,因为我编写了方法或检查了方法,是否应该跳过对0的检查?
Didier A.

@didibus:您可以跳过检查是否getCount() 记录了现在不会返回0的保证,并且将来不会这样做,除非有人决定进行重大更改并打算更新所有需要它来应对的情况新界面。看到代码当前没有帮助,但是如果您要检查代码,则要检查的代码是的单元测试getCount()。如果他们测试它不返回0,那么您知道将来返回0的任何更改都将被视为重大更改,因为测试将失败。
史蒂夫·杰索普

对我来说,尚不清楚空对象是否比空对象更好。空的鸭子可能会发出嘎嘎声,但它不是鸭子-实际上,从语义上讲,这是鸭子的对立面。引入空对象后,使用该类的任何代码都可能必须检查其任何对象是否为空对象-例如,您在集合中具有空值,那么您有多少东西?在我看来,这似乎是推迟失败并消除错误的一种方法。链接的文章特别警告不要使用空对象,“只是为了避免空检查,并使代码更具可读性”。
sdenham

1

通常,我会说这主要取决于以下三个方面:

  1. 健壮性:调用方法可以处理该null值吗?如果一个null值可能会导致RuntimeException我建议检查-除非复杂度低并且被调用/调用方法是由同一作者创建的,并且null不是预期的(例如,两者都private在同一包中)。

  2. 代码责任:如果开发人员A负责该getUser()方法,而开发人员B使用它(例如,作为库的一部分),则强烈建议您验证其值。仅仅因为开发者B可能不知道导致潜在null返回值的更改。

  3. 复杂度:程序或环境的总体复杂度越高,我建议验证返回值的建议就越多。即使您确定null在调用方法的上下文中它今天不可能存在,也可能是您必须getUser()为另一个用例进行更改。一旦消失了几个月或几年,并且添加了几千行代码,这可能是一个陷阱。

另外,我建议null在JavaDoc注释中记录潜在的返回值。由于大多数IDE中都突出显示了JavaDoc描述,因此无论对谁使用,这都是一个有用的警告getUser()

/** Returns ....
  * @param: id id of the user
  * @return: a User instance related to the id;
  *          null, if no user with identifier id exists
 **/
User getUser(Int id)
{
    ...
}

-1

您绝对不必。

例如,如果您已经知道返回永远不能为空-为什么要添加空检查?添加空检查不会破坏任何东西,但这只是多余的。最好不要编写冗余逻辑,只要您可以避免它。


1
我知道,但这只是因为我检查或编写了另一种方法的实现。该方法的合同未明确表明不能这样做。因此,它可能会在将来发生变化。或者它可能有一个错误,即使它不尝试执行,也可能导致返回null。
Didier A.
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.