设计代码时将空集合和null值区分对待是好还是坏的做法?

5

我正在处理的项目的某个部分得到了更新,有些人开始将空集合作为参数发送到方法中,而不是null

这导致首先出现了一个bug,然后我用if (CollectionUtils.isEmpty(myCollection))替换了if (null == myCollection),最终导致了一系列bug。通过这种方式,我发现很多代码对这些集合的处理方式是不同的:

  • 当一个集合是空的时(即用户明确希望在这里什么也没有)
  • 当一个集合是null时(即用户在这里没有提到任何内容)

因此,我的问题是:这是好的设计实践还是坏的设计实践?


5
这要看情况而定...... - Maurice Perry
@MauricePerry 这取决于具体的情况是什么?? 很好的回答。非常随机。 - BionicCode
@OleV.V. 当getPersons()返回一个person集合时,在什么情况下您会期望该集合为NULL?如果没有人,则集合为空。调用者则知道“啊哈,没有找到人”。对于调用者来说,NULL意味着什么?什么也不意味着。在出现错误的情况下,最好抛出异常。这更有意义。 - BionicCode
@BionicCode 或许我忘了提到:在我看来,必须明确记录“null”的含义是非常清晰的前提条件。就我所理解的问题而言,“null”并不意味着错误,而更像是“未知”。 - Ole V.V.
@OleV.V. 在集合方面,我们可以说Null始终是可避免的。当有人返回NULL时,这只是表达了松散和危险的代码,可能会将一些未定义的对象引用返回给调用者。为什么要这样做呢?如果您的类或方法处于未定义状态,则应用程序变得不可预测。这时抛出异常是适当的。 - BionicCode
显示剩余4条评论
6个回答

11
在他(非常好的)著作《Effective Java》中,Joshua Bloch针对方法返回值的问题进行了讨论(不像您的问题“一般性”的),具体是关于使用null的问题:
使用null是容易出错的,因为编写客户端程序的程序员可能会忘记编写处理null返回值的特殊情况代码。
有时人们认为null返回值比空数组更可取,因为它避免了分配数组的开销。但这个论点有两个缺点。首先,在此级别上担心性能是不明智的,除非分析表明该方法是性能问题的真正原因(第55条)。其次,可以从每个返回零项的调用中返回相同的零长度数组,因为零长度数组是不可变的,而不可变对象可以自由共享(第15条)。
总之,从数组或集合值方法中返回空数组或集合,而不是返回null,没有任何理由。(...)
就我个人经验而言,我在代码中对任何使用Collections的情况都采用这种推理作为指导方针。当然,有些情况下区分null和empty是有意义的,但这种情况相当罕见。

然而,如BionicCode在评论部分所述,在方法返回null而不是空值以指示出现错误的情况下,您总是有抛出异常的可能性。


在集合的情况下,返回 NULL 而不是空集合是没有意义的。如果执行或生成集合不可能,则应抛出异常(例如 ArgumentException)。这比仅返回 NULL 更有意义。 - BionicCode
如果您能在错误情况下添加抛出异常的选项而不是返回NULL,那么我会给您点赞。在这种情况下,完全可以避免使用NULL。为了可读性、代码含义和最重要的健壮性,必须避免使用NULL。 - BionicCode
1
@BionicCode 很好的建议,我已经将您的建议添加到答案中。谢谢。 - Vincent Passau
由于黄色框和其中的最后一行加粗字体,您应该得到更多的赞。但似乎这个想法太难理解了,或者打破旧习惯太痛苦了,因此更容易否认它。 - BionicCode

2
我知道这个问题已经有了一个被接受的答案。但是由于我进行的讨论,我决定写下自己的答案来解决这个问题。
NULL的发明者Tony Hoare先生说过:
“我称它为我的十亿美元错误。这是1965年引入null引用的发明。那时,我正在设计面向对象语言(ALGOL W)中引用的第一个全面类型系统。我的目标是确保所有引用的使用都是绝对安全的,并由编译器自动执行检查。但我无法抵制放置空引用的诱惑,仅仅因为它很容易实现。这导致了无数的错误、漏洞和系统崩溃,可能在过去四十年里造成了十亿美元的痛苦和损失。”
首先,在更新已发布和测试过的旧代码时引入破坏性更改是非常不好的做法。在这种情况下进行重构总是很危险的,应该避免。我知道发布丑陋或愚蠢的代码可能会伤害你的感情,但如果出现这种情况,那么这清楚地表明缺乏质量管理工具(例如代码审查或规范)。但显然没有单元测试,否则作者会由于测试失败而立即撤销更改。主要问题在于你的团队没有约定如何处理NULL作为参数或结果。更新者将从NULL参数切换到空集合的意图是绝对正确的,应该得到支持,但他不应该在工作代码中这样做,并且他应该与团队讨论,以便团队的其他成员可以跟进,以使其更有效。你的团队必须一致同意放弃将NULL作为参数或结果值,或至少找到一个标准。最好把NULL送到地狱去。你唯一的解决方案是撤销更新作者所做的更改(我假设你正在使用版本控制)。然后,用旧式且肮脏的方式使用NULL进行更新,就像以前一样。不要在新代码中使用NULL-为了更美好的未来。试图修复已更新的版本肯定会加剧情况,因此会浪费时间。(我假设我们正在谈论一个更大的项目)。如果可能的话,请回滚到上一个版本。
简言之,如果您不喜欢继续阅读:是的,这是非常糟糕的做法。您自己可以从目前的情况中得出这个结论。现在,您目睹了非常不稳定和不可预测的代码。无理性的错误是证明您的代码变得不可预测的证据。如果您至少没有业务逻辑的单元测试,那么代码就像热铁一样。导致这种代码的做法永远不可能是好的。NULL对API的用户没有直观意义,当有可能得到中性结果或参数时(例如集合的情况),则应该优先考虑使用中性结果或参数。空集合是中性的。您可以期望一个集合是空的或至少包含一个元素。从集合中您不能期望其他任何东西。你想表明一个错误和一个操作无法终止?那么最好抛出一个带有清晰根源的良好名称的异常,以向调用者清楚地传达错误的根本原因。
NULL是一个历史遗留物。在程序员跌倒时,NULL值意味着程序员编写了松散的代码。他忘记通过将内存地址赋值给指针来初始化指针。编译器必须知道内存地址作为参考,以创建有效的指针。如果你想要一个指针但不想浪费内存仅用于声明指针或者此时还不知道地址,则NULL是一种约定使得指针指向不存在的地方,这意味着不必为指针的引用分配内存(除了指针本身)。如今,在具有垃圾回收和大量可用内存的现代面向对象语言中,NULL已经变得不相关了。存在某些情况(如在SQL中)需要使用它以表示缺少数据。但在面向对象编程中,您完全可以避免使用它,从而使您的应用程序更加健壮。
您可以选择应用Null-Object模式。此外,最佳实践是使用默认参数(如果您的语言支持),或使用重载。例如,如果方法参数是可选的,则使用重载(或默认参数)。如果该参数是强制性的,但您无法提供值,则只需不调用该方法。如果该参数是集合,则在没有值的情况下始终使用空集合。方法的作者必须处理此情况,因为他必须处理所有可能的情况或参数状态,包括NULL。因此,方法的作者有责任检查NULL并决定如何处理它。这就是惯例发挥作用的地方。如果您的团队同意永远不使用NULL,则不再需要进行繁琐且丑陋的空值检查。一些框架提供@NotNull属性。作者可以使用它来装饰方法参数以指示NULL不是有效值。编译器现在将执行NULL检查并向(误用)该方法的程序员显示错误消息,以防止或识别违规操作,并导致更加健壮的代码。
大多数库都会提供帮助类,例如Array.Empty()Enumerable.Empty()(C#),用于创建提供IsEmpty()等方法的空集合。这使意图在语义上变得清晰,从而使代码易于阅读。如果您的标准库中没有这样的帮助程序,则编写自己的帮助程序是值得的。
尝试将质量管理纳入团队的常规工作流程中。进行代码审查,以确保要发布的代码符合您的质量标准(单元测试,无NULL值,始终对语句体使用大括号,命名等)。
我希望你能解决问题。我知道清理他人的混乱是一种有压力的情况。这就是为什么团队间的沟通如此重要的原因。

1
我们的意见其实比我一开始想象的更为一致。感谢您提供了一份详尽而明智的答案。 - Ole V.V.
1
如果 null 不存在,那么就必须发明它。(向伏尔泰致歉。) - Stuart Marks
1
@StuartMarks 谢谢,这个很好。你觉得这个怎么样:“不要告诉我 null 和 space 之间没有任何区别,因为这正是它们之间的区别。” Larry Wall - 检查表格 - BionicCode

1

建议在集合的情况下使用以下方式。

if(myCollection !=null && !myCollection.isEmpty()) {

//Process the logic

}

作为设计的一部分,Joshua Bloch建议在Effective Java中使用空集合。
引用他的话:
“从返回零长度数组而不是从数组值方法返回null没有任何理由。”
您可以在此处找到Effective Java的链接。https://www.amazon.com/dp/0321356683

1

这取决于您的需求。Null绝对不是空集合。我认为,将空集合和非空集合分开处理是一种不好的做法。但是,Null表示一种缺乏数据的情况。我认为,如果这种情况是合法的,并且您正在使用Java 8或更高版本,则应该使用Optional。在这种情况下,Optional.empty()表示没有集合,而Optional.of(collection)表示即使它本身为空,集合也存在。


1
如果只是一个方法的返回值,我才会使用 Optional。从问题描述来看,好像不是这种情况。否则,我同意你的看法。 - Ole V.V.
另外,如果OP使用的是Java 7或更早版本,则可以使用类库(如Guava)来访问Optional及其功能。 - Tom
将空集合返回NULL是没有意义的。如果我调用一个方法来返回一组对象,但没有对象可返回,则结果集合保持为空。这在语义上更正确,并且每个调用者都期望如此。当方法由于不正确的参数等原因无法执行时,最好抛出ArgumentException而不是返回NULL。 - BionicCode
NULL几乎从不是必要的,也不是预期或期望的结果。NULL表示没有分配内存或引用未初始化(指向无处)。该方法必须确保所有返回的对象都正确初始化,如果无法初始化,则抛出异常。 - BionicCode

1

访问空值时,您可能会直接获得NullPointerException,并且在逻辑的更深层次中,大多数情况下(根据我的经验),将null值与空集合区分开来是不好的实践。应该为空而不是null。


0

让我们在这里放一些下投票的饵料,好吗?据我所知,你有一个像这样的方法

public void foo(Collection myCollection) {
    if (CollectionUtils.isEmpty(myCollection)) {
        // ...
    }
    // ...
}

我也明白这个方法被很多地方调用,所以如果你不需要改变所有的调用,那么最实际的方法就是不需要改变。我也明白传递null和传递一个空集合之间存在着语义上的差异。一个空集合意味着我们确切知道没有元素。而null则意味着没有指定是否有任何元素(我假设你的方法在这种情况下也能够执行有用的工作)。

好的设计应该有两个方法:

/** @param myCollection a possibly empty collection; not null */
public void foo(Collection myCollection);

/** Call this method if you don’t want to specify elements */
public void foo();

然而,考虑到您现有的代码库,您不想突然引入提到的非空要求,并破坏一直有效的代码。一种前进的方法是在1-arg方法上引入注释,有效地说

将null传递给此方法已过时。它可以工作,但可能会在将来的版本中被禁止。

这将为您购买时间,在未来几个月甚至几年内更改代码库,同时仍然允许您在某些时间点到达更好的设计。

与此同时,如果1-arg方法收到null,则可以将其更改为仅委派给no-arg方法。

注意:您需要在文档(Javadoc)中非常清楚地说明不指定元素(理想情况下调用no-arg方法)的语义,以及与传递空集合的语义差异。


情况一:一个方法需要对元素集合进行操作,但你没有任何元素?那么要么不调用该方法,要么只传入一个空的集合。情况二:一个方法必须返回一个集合作为操作结果,但没有元素可以返回或者由于某种原因该方法无法操作?那么要么不返回任何内容并抛出有意义的异常,要么返回一个空的集合。因此,您不再需要在代码中添加丑陋的NULL检查,也不会污染您的代码,显然您的应用程序变得更加健壮。 - BionicCode
因此,这个问题的解决并不是设计问题,而是程序员的自律和思考能力的问题。 - BionicCode
许多框架都提供@NotNull属性,API的编写者可以用它来装饰方法参数和字段。目的是强制程序员坚持最佳实践。否则,你将会得到一个编译器错误。很遗憾,这些属性是必要的,因为远离NULL应该是自然而然的,因为NULL本身就是一个严重的异常。它曾经意味着程序员工作马虎,忘记为指针分配内存地址,或者指针引用的内存已经不再分配。 - BionicCode
大多数编程语言都提供了像 Array.Empty()Collection.Empty() 这样的辅助方法,以强调空集合值给读者带来的意义。这样阅读起来非常愉快,因为意图非常清晰。 - BionicCode
1
感谢您的耐心等待。我认为我们都理解了同样的问题。我并没有回答他的问题,而是评论了一些在我看来应该强调NULL始终是一个不好的实践的答案。正是因为有NULL,他才遇到了所有的问题。如果没有NULL,任何方法的意图都会更加清晰明了。我想说的是,问题比仅仅提供重载更加复杂。他的团队需要更好的沟通和质量标准(放弃NULL就是其中之一)。我写了一个答案,所以我不需要污染你的答案。请随意发表您的意见。 - BionicCode
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接