返回null是不好的设计吗?

142

我听到有些声音说从方法返回null值并进行检查是不好的设计。我希望听到一些原因。

伪代码:

variable x = object.method()
if (x is null) do something

14
详细说明一下:那些说这很糟糕的人在哪里?有链接吗? - jcollum
2
如果这个方法是你可以控制的,你可以编写单元测试来确保它永远不会返回 null。否则,我认为在调用之后检查是否为 null 并不是一种不好的做法;虽然该方法返回 null 可能是一种不好的做法,但你必须保护你的代码。 - BlackTigerX
9
仅仅因为没有数据返回而引发异常是非常烦人的。正常程序流程不应该抛出异常。 - Thorarin
4
@David:其实这就是我说的。如果一个方法应该返回数据,但没有返回任何数据,那也意味着出了问题。这不是正常的程序流程 :) - Thorarin
2
@Thorarin:“正常”的程序流程是一个相当灵活的概念:不是真正的论据基础。 - Tomislav Nakic-Alfirevic
显示剩余6条评论
24个回答

222
不返回null的原因在于您不必检查它,因此您的代码不需要根据返回值跟随不同的路径。您可能想了解更多关于“Null对象模式”的信息,请查看Null Object Pattern
例如,如果我在Java中定义一个返回Collection的方法,我通常会选择返回一个空的集合(即Collections.emptyList()),而不是null,因为这意味着我的客户端代码更加简洁。
Collection<? extends Item> c = getItems(); // Will never return null.

for (Item item : c) { // Will not enter the loop if c is empty.
  // Process item.
}

...这比以下方式更加简洁:

Collection<? extends Item> c = getItems(); // Could potentially return null.

// Two possible code paths now so harder to test.
if (c != null) {
  for (Item item : c) {
    // Process item.
  }
}

8
比起返回空值并希望客户端记得处理这种情况,这样做好多了。 - djna
13
我很乐意同意,当返回null作为空容器(或空字符串)的替代时,这是不合理的。不过这并不是常见情况。 - MSalters
3
对我来说,Null对象模式也是+1。 当我确实想要返回null时,我会像getCustomerOrNull()这样给方法命名,以使其明确。 我认为当读者不想查看实现时,方法被命名得很好。 - Mike Valenty
4
“// Two possible code paths now” 这条注释是不正确的;无论如何,你都有两个代码路径。但是,在第一个示例中使用 null 集合对象时,“null”代码路径更短。尽管如此,你仍然有两个路径,需要测试两个路径。 - Frerich Raabe
1
说实话,空对象模式听起来像是一个可怕的想法。想象一下,你必须为类层次结构中的每个节点创建一个特殊的 Null 类,在需要立即使用 Null 对象的情况下!我认为当期望空对象时更好的做法是检查,例如 if (c == null):如果为真,则退出代码(_返回 false、抛出异常、回滚等_);否则(独立的代码块,而不是 else 块!),继续执行。 - ADTC
显示剩余6条评论

87

这是原因。

Clean Code by Robert Martin中,他写道,当你可以返回空数组时,返回null是不好的设计。既然预期结果是一个数组,为什么不呢?这将使您能够在没有任何额外条件的情况下迭代结果。如果它是一个整数,也许0就足够了,如果是一个哈希表,就是一个空的哈希表等等。

前提是不要强制调用代码立即处理问题。调用代码可能不想关注它们。这也是为什么在许多情况下,异常比nil更好的原因。


2
这是在这个回答中提到的Null对象模式:https://dev59.com/TXM_5IYBdhLWcg3wrFMt#1274822 - Scott Dorman
这里的想法是,在出现错误时,返回一个空/空白版本的正常情况下应该返回的任何对象类型。例如,对于这些情况,空数组或字符串都可以使用。当您通常返回指针时(因为NULL本质上是一个空指针),返回“NULL”是适当的。从通常返回哈希值的函数中返回NULL可能会令人困惑。一些语言处理得比其他语言更好,但总的来说,保持一致性是最佳实践。 - bta
除非错误在某种程度上控制了流程(这不是一种好的实践)或者在 API 或接口中进行了抽象处理,否则您不必返回错误。 错误会传播到您决定捕获它的任何级别,因此您不必在调用上下文中处理它。 它们默认情况下符合 null-object-pattern-friendly。 - Max Chernyak
不幸的是,调用代码并不总是考虑空集合的情况(就像它不考虑空值情况一样)。这可能是一个问题,也可能不是。 - Sridhar Sarnobat

40

返回null的好处:

  • 如果 null 是一个有效的 功能 结果,例如:FindFirstObjectThatNeedsProcessing() 如果未找到可以返回 null ,则调用者应相应地进行检查。

使用不当的情况:

  • 捕获(catch(...))异常并返回 null
  • API 依赖项初始化失败
  • 磁盘空间不足
  • 无效的输入参数(编程错误,调用者必须对输入进行清理)
  • 等等

在这些情况下,抛出异常更为适当,因为:

  • null 返回值没有提供有意义的错误信息
  • 直接的调用者可能无法处理错误条件
  • 不能保证调用者正在检查 null 结果

但是,异常不应用于处理正常的程序操作条件,例如:

  • 无效的用户名/密码(或任何用户提供的输入数据)
  • 中断循环或作为非本地跳转

7
"Invalid username" 看起来是一个很好的异常原因。 - Thilo
14
输入无效的登录密码不应视为异常情况,这是正常程序操作的一部分。然而,身份验证系统未能响应(例如,活动目录)是一种异常情况。 - ZeroConcept
2
我会这样说,这取决于情况:boolean login(String,String) 看起来不错,AuthenticationContext createAuthContext(String,String) throws AuthenticationException 也不错。 - sfussenegger
2
在您的示例中,如果没有需要处理的第一个对象,则有两种合理的处理方式。第一种是Null Object模式。创建一个最终子类,表示该类的不存在版本。它具有合理的默认值,并在请求无意义操作时引发异常。另一种技术是Option。这是Java8和Scala中可用的内容。这两种方法都显示了开发人员的意图。null无法显示意图,因为它有太多可能的含义。显示意图是代码最重要的方面。 - scott m gardner
1
我认为除了第一条语句“FindFirstObjectThatNeedsProcessing() can return null if not found and the caller should check accordingly.”之外,其他都可以同意。函数的名称字面上告诉我它将返回一个对象,而null不是一个对象。如果未找到对象,则似乎可以抛出异常。 - JaredCS
显示剩余2条评论

39

在面向对象的世界中,返回NULL是一种糟糕的设计。简而言之,使用NULL会导致以下问题:

  • 临时错误处理(而不是异常)
  • 歧义语义
  • 缓慢而非快速失败
  • 计算机思维而非对象思维
  • 可变和不完整的对象

请参阅此博客文章以获取详细解释:http://www.yegor256.com/2014/05/13/why-null-is-bad.html 。更多内容请参见我的书籍 Elegant Objects ,第4.1节。


3
我非常同意。我甚至不喜欢看到空对象模式,这似乎是一种掩盖延迟策略。你的方法要么应该总是成功,要么可能会失败。如果它应该总是成功,则应该抛出异常。如果可能会失败,则应该设计方法使消费者知道,例如 bool TryGetBlah(out blah)FirstOrNull()MatchOrFallback(T fallbackValue) - Luke Puplett
我也不喜欢返回null。这样会把根本原因和症状分开,使得调试更加困难。要么抛出异常(快速失败),要么先调用一个返回布尔值的检查方法(例如isEmpty()),只有在它为真时才调用该方法。有人反对第二种方法,认为它性能较差 - 但正如Unix哲学所说,“珍惜人类时间胜过机器时间”(即微不足道的较慢性能浪费的时间比开发人员调试给出虚假错误的代码更少)。 - Sridhar Sarnobat

21

谁说这是不好的设计?

检查空值是一种常见的做法,甚至是被鼓励的,否则你就会在各个地方面临 NullReferenceExceptions 的风险。最好优雅地处理错误而不是在不需要抛出异常时抛出。


12
抛出异常来处理可以立即解决的问题的代码让我感到悲伤。 - jkeys
7
当程序员忘记检查空值并出现神秘的空指针异常时,你会感到多么难过?编译器提醒用户未处理错误条件的已检查异常可以避免这类编码错误。 - djna
3
@djna说:我猜这也很令人难过,但我发现那些“忘记”检查空值的程序员通常会在处理已检查异常时经常吞掉它们。 - Randy Levy
5
发现存在空的catch块比发现缺少null检查要容易。 - Preston
22
@Preston: 我强烈反对。如果没有空值检查,当程序崩溃时您会立即发现。而被吞没的异常可能会在多年后传播出神秘和微妙的错误... - Beska
显示剩余6条评论

19

根据您目前提供的信息,我认为还不够充分。

从CreateWidget()方法返回null似乎不好。

从FindFooInBar()方法返回null似乎没问题。


4
类似于我的约定:单项查询 - Create... 返回一个新的实例,或抛出异常Get... 返回一个预期存在的实例,或抛出异常GetOrCreate... 返回一个现有实例,如果不存在则返回新实例,或抛出异常Find... 返回一个现有实例,如果存在,则返回该实例,否则返回null对于集合查询 - Get... 始终返回一个集合,如果没有匹配的条目,则为空。 - Johann Gerell
NULL的问题在yegor256和hakuinin的回答中已经解释了,返回一个有效但空的对象可以简化推理。 - Rob11311

16

它的发明者表示,这是一个价值十亿美元的错误!


10

这取决于你使用的编程语言。如果你使用的是像C#这样的语言,其中表示缺少值的惯用方式是返回null,那么如果没有值,返回null是一个好的设计。相反,在像Haskell这样的语言中,惯用的方式是使用Maybe Monad处理这种情况,那么返回null将是一个不好的设计(即使可能性很小)。


2
如果提到了Maybe Monad,我会给你点赞。我发现在C#和Java等语言中,null的定义经常被重载并在特定领域中赋予某些含义。如果你查阅语言规范中的null关键字,它只是表示“一个无效指针”。这在任何问题领域中都可能意味着什么都不代表。 - MattDavey
问题在于你不知道它是“缺失值”null还是“未初始化”null - CervEd

6

在这个领域,我有一个惯例,它对我很有帮助

对于单项查询:

  • Create... 返回一个新实例,或抛出异常
  • Get... 返回一个预期的现有实例,或抛出异常
  • GetOrCreate... 返回一个现有实例,如果不存在,则返回新实例,或抛出异常
  • Find... 返回一个现有实例(如果存在),否则返回 null

对于集合查询:

  • Get... 总是返回一个集合,如果没有找到匹配项,则为空

[1] 给定一些标准,明确或隐含,在函数名或参数中给出。


为什么 GetOne 和 FindOne 如果没有找到会返回不同的结果? - Vladimir Vukanac
1
我描述了它们的区别。当我期望某个值存在时,我使用Get方法,这样如果它不存在,就会抛出一个错误 - 我不需要检查返回值。而当我真的不确定某个值是否存在时,我使用Find方法 - 这时我需要检查返回值。 - Johann Gerell

6
如果您阅读所有答案,就会清楚这个问题的答案取决于方法的类型。
首先,在发生异常情况(如IO问题等)时,逻辑上会抛出异常。什么情况下算是异常情况可能是另一个话题...
每当期望某个方法可能没有结果时,有两种情况:
- 如果可能返回中性值,则返回中性值。空的枚举、字符串等都是很好的例子。 - 如果不存在这样的中性值,则应该返回 null。正如前面提到的,假定该方法可能没有结果,因此不是异常情况,因此不应该抛出异常。中性值不可能存在(例如:0并不是特别中性的结果,这取决于程序)。
在我们有官方的方法来表示一个函数是否可以返回 null 之前,我尝试使用一种命名约定来表示这一点。就像你对那些预计失败的方法使用 TrySomething() 约定一样,当方法返回中性结果而不是 null 时,我经常将我的方法命名为 SafeSomething()。
我对这个名称还不完全满意,但是无法想出更好的名称,所以现在就用这个名称。

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