ChatGPT解决这个技术问题 Extra ChatGPT

是否返回 null 糟糕的设计? [关闭]

关闭。这个问题是基于意见的。它目前不接受答案。想改进这个问题?更新问题,以便可以通过编辑这篇文章用事实和引用来回答它。 3年前关闭。改进这个问题

我听到一些声音说从方法中检查返回的空值是不好的设计。我想听听一些原因。

伪代码:

variable x = object.method()
if (x is null) do something
详细说明:这些说不好的人在哪里?链接?
如果该方法是您可以控制的,您可以进行单元测试以确保它永远不会返回 null,否则,我不明白为什么在调用之后检查它是否为 null 是一种不好的做法;在该方法上返回 null 可能是一种不好的做法,但您必须保护您的代码
仅仅因为没有要返回的数据而引发异常是非常烦人的。正常的程序流不应抛出异常。
@David:这就是我所说的。如果一个方法应该返回数据,但没有,这意味着也出了问题。这不是正常的程序流程:)
@Thorarin:“正常”程序流程是一个相当可扩展的概念:并不是一个论证的坚实基础。

A
Adamski

不返回 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.
  }
}

是的,这比返回 null 并希望客户记得处理此案要好得多。
我很高兴地同意,当它被用作空容器(或字符串)的替代品时,返回 null 是疯狂的。不过,这不是常见的情况。
对我来说也为空对象模式+1。此外,当我实际上想要返回 null 时,我一直在命名 getCustomerOrNull() 之类的方法以使其明确。我认为当读者不想查看实现时,方法命名得很好。
注释 '// 现在有两个可能的代码路径' 不正确;无论哪种方式,您都有两个代码路径。但是,对于第一个示例中的 null Collection 对象,“null”代码路径更短。但是,您仍然有两条路径,并且您仍然需要测试两条路径。
@MSalters - 可以说 每个 OO 语言中带有 null 的变量是一个“容器”,它包含零个或一个指向对象的指针。 (这当然是 Haskell 的 Maybe 在这些情况下明确建模的方式,这样做会更好。)
M
Max Chernyak

这就是原因。

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

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


这是此答案中提到的空对象模式:stackoverflow.com/questions/1274792/…
这里的想法是,如果发生错误,您将返回正常返回的任何对象类型的空/空白版本。例如,空数组或字符串适用于这些情况。当您通常会返回一个指针时,返回“NULL”是合适的(因为 NULL 本质上是一个空指针)。从通常返回哈希的函数中返回 NULL 可能会令人困惑。有些语言比其他语言处理得更好,但总的来说,一致性是最佳实践。
您不一定会返回错误,除非错误以某种方式控制流程(这不是一个好的做法)或在 API 或接口中抽象。错误可以传播到您决定捕获它的任何级别,因此您不必在调用上下文中处理它。默认情况下,它们是空对象模式友好的。
不幸的是,调用代码并不总是考虑空集合的情况(就像它不考虑 null 情况一样)。这可能是也可能不是问题。
Z
ZeroConcept

返回 null 的良好用途:

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

不良用途:试图替换或隐藏异常情况,例如:

catch(...) 并返回 null

API 依赖初始化失败

磁盘空间不足

无效的输入参数(编程错误,输入必须由调用者清理)

ETC

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

空返回值不提供有意义的错误信息

直接调用者很可能无法处理错误情况

不能保证调用者正在检查空结果

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

无效的用户名/密码(或任何用户提供的输入)

打破循环或作为非本地 goto


不过,“无效的用户名”似乎是引发异常的一个很好的原因。
输入无效登录名/密码的用户不应被视为例外情况,这是正常程序操作的一部分。但是,身份验证系统未能响应(例如活动目录)是一种例外情况。
我会说这取决于:boolean login(String,String) 看起来不错,AuthenticationContext createAuthContext(String,String) throws AuthenticationException 也不错
在您的示例中,如果没有需要处理的第一个对象,那么有两种合理的方法来处理这种情况。第一个是空对象模式。创建一个代表该类的不存在版本的最终子类。它具有合理的默认值,并在请求无意义的操作时抛出异常。另一种技术是选项。这是 Java8 和 Scala 中可用的。这些都显示了开发人员的意图。 null 不能显示意图,因为它有太多可能的含义。 ** 显示意图** 是代码最重要的方面。
y
yegor256

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

临时错误处理(而不是异常)

模棱两可的语义

缓慢而不是快速失败

计算机思维代替对象思维

可变和不完整的对象

查看此博文了解详细说明:http://www.yegor256.com/2014/05/13/why-null-is-bad.html。更多内容见我的书 Elegant Objects,第 4.1 节。


我强烈同意。我什至不喜欢看起来是“粉饰”延迟策略的空对象模式。您的方法要么总是成功,要么可能不会。如果它应该总是成功,那么抛出。如果它可能不成功,则设计方法让消费者知道,例如 bool TryGetBlah(out blah)FirstOrNull()MatchOrFallback(T fallbackValue)
我也不喜欢返回null。您将根本原因与症状分开,使调试更加困难。要么抛出异常(快速失败),要么调用首先返回布尔值的检查器方法(例如 isEmpty()),并且只有当它为真时才调用该方法。人们反对第二个,说它的性能更差——但正如 Unix 哲学所说,“重视人的时间而不是机器的时间”(即,与开发人员调试产生虚假错误的代码相比,性能稍慢一点所浪费的时间更少)。
B
Brandon

谁说这是糟糕的设计?

检查空值是一种常见的做法,甚至受到鼓励,否则你会到处冒着 NullReferenceExceptions 的风险。与其在不需要时抛出异常,不如优雅地处理错误。


+1。对可以当场缓解的问题抛出异常的代码让我很难过。
当编码人员忘记检查空值然后得到神秘的空指针异常时,你会有多难过?检查异常,编译器会提醒用户他们没有处理错误情况,以避免发生此类编码错误。
@djna:我想这也很可悲,但我发现那些“忘记”检查空值的编码器是那些在处理检查异常时经常最终吞下它们的编码器。
发现空 catch 块的存在比不存在空检查更容易。
@Preston:我强烈反对。当它崩溃时,您会立即发现没有空检查。吞下的异常可以传播多年的神秘和微妙的错误......
j
jcollum

根据你到目前为止所说的,我认为没有足够的信息。

从 CreateWidget() 方法返回 null 似乎很糟糕。

从 FindFooInBar() 方法返回 null 似乎很好。


与我的约定类似:单项查询 - Create... 返回一个新实例,或抛出Get... 返回预期的现有实例,或抛出GetOrCreate... 返回现有实例,如果不存在则返回新实例,或抛出Find... 返回现有实例(如果存在)null对于集合查询 - Get... 始终返回一个集合,如果未找到匹配项,则该集合为空。
由于 yegor256 和 hakuinin 的回答中给出的原因,NULL 不好,返回一个有效但空的对象简化了推理
B
Bahadır Yağan

它的发明者 says 这是一个十亿美元的错误!


G
Greg Beech

这取决于您使用的语言。如果您使用像 C# 这样的语言,其中指示缺少值的惯用方式是返回 null,那么如果您没有值,则返回 null 是一个不错的设计。或者,在 Haskell 等语言中,在这种情况下习惯性地使用 Maybe monad,然后返回 null 将是一个糟糕的设计(如果它甚至可能的话)。


+1 提到也许是单子。我发现 null 在 C# 和 Java 等语言中的定义经常被重载并在域中被赋予了一些含义。如果您在语言规范中查找 null 关键字,它仅表示“无效指针”。这可能在任何问题领域都没有任何意义。
问题是您不知道它是“缺失值”null 还是“未初始化”null
B
Boris Callens

如果您阅读了所有答案,就会清楚这个问题的答案取决于方法的种类。

首先,当发生异常情况(IOproblem 等)时,逻辑上会抛出异常。当某些事情确实很特别时,可能是针对不同主题的事情。

每当一种方法预期可能没有结果时,有两类:

如果可以返回中性值,请执行此操作。空枚举、字符串等都是很好的例子

如果不存在这样的中性值,则应返回 null。如前所述,假设该方法可能没有结果,因此它不是异常,因此不应抛出异常。中性值是不可能的(例如:0 不是特别中性的结果,取决于程序)

直到我们有一个正式的方式来表示一个函数可以或不能返回 null,我尝试有一个命名约定来表示。就像您对预期会失败的方法有 TrySomething() 约定一样,当方法返回中性结果而不是 null 时,我经常将方法命名为 SafeSomething()。

我对这个名字还不完全满意,但想不出更好的名字。所以我现在正在使用它。


J
Johann Gerell

我在这个领域有一个对我很有帮助的会议

对于单项查询:

Create... 返回一个新实例,或抛出

Get... 返回预期的现有实例,或抛出

GetOrCreate... 返回现有实例,如果不存在则返回新实例,或抛出

Find... 返回现有实例(如果存在)或 null

对于集合查询:

Get... 总是返回一个集合,如果没有找到匹配的 [1] 个项目,则该集合为空

[1] 给定一些标准,显式或隐式,在函数名称中或作为参数给出。


如果找不到,为什么 GetOne 和 FindOne 返回不同?
我描述了差异。当我期望它存在时,我使用 Get,所以如果它不存在,那么它就是一个错误并且我抛出 - 我永远不需要检查返回值。如果我真的不知道它是否存在,我使用 Find - 然后我需要检查返回值。
M
Michael Burr

这不一定是一个糟糕的设计 - 就像许多设计决策一样,这取决于。

如果该方法的结果在正常使用中不会产生好的结果,则返回 null 是可以的:

object x = GetObjectFromCache();   // return null if it's not in the cache

如果真的应该总是有一个非空结果,那么最好抛出一个异常:

try {
   Controller c = GetController();    // the controller object is central to 
                                      //   the application. If we don't get one, 
                                      //   we're fubar

   // it's likely that it's OK to not have the try/catch since you won't 
   // be able to really handle the problem here
}
catch /* ... */ {
}

尽管您可以在此处记录诊断错误,但可能会重新引发异常。有时,首次故障数据捕获可能会很有帮助。
或者将异常包装在 RuntimeException 中,并在消息字符串中包含诊断信息。将信息放在一起。
现在(2018 年)我不能再同意了,在这种情况下,我们应该支持返回 Optional<>
佚名

例外是针对特殊情况。

如果您的函数旨在查找与给定对象关联的属性,并且该对象确实没有此类属性,则返回 null 可能是合适的。如果对象不存在,则抛出异常可能更合适。如果该函数旨在返回一个属性列表,并且没有要返回的属性,则返回一个空列表是有意义的——您将返回所有零属性。


如果您尝试使用没有值的属性,则值得例外。 (我假设您的规范说该属性是可选的。)有一个单独的方法来检查其值是否已设置。
S
Steve B.

如果这样做在某种程度上有意义,则可以返回 null:

public String getEmployeeName(int id){ ..}

在这种情况下,如果 id 与现有实体不对应,则返回 null 是有意义的,因为它允许您将未找到匹配项的情况与合法错误区分开来。

人们可能认为这很糟糕,因为它可能被滥用为指示错误条件的“特殊”返回值,这不太好,有点像从函数返回错误代码但令人困惑,因为用户必须检查返回null,而不是捕获适当的异常,例如

public Integer getId(...){
   try{ ... ; return id; }
   catch(Exception e){ return null;}
}

是的。如果您遇到错误情况,请抛出异常。
这是一种需要例外的情况。如果您请求的员工姓名不存在,那么显然出了问题。
我认为这有点字面意思,Thorarin——你当然可以用我的例子来狡辩,但我相信你可以想象一个函数的一些实例返回一个匹配的值,或者如果没有匹配则返回 null。如何从哈希表中的键获取值? (应该首先想到那个例子)。
为不存在的键返回 null 的哈希表是错误的。自动这样做意味着您不能将 null 存储为没有不一致代码的值。
D
Drew Hoskins

对于某些情况,您希望在故障发生时立即注意到它。

在失败案例中检查 NULL 并且不断言(针对程序员错误)或抛出(针对用户或调用者错误)可能意味着以后的崩溃更难追踪,因为没有找到原始的奇怪案例。

此外,忽略错误可能会导致安全漏洞。也许空值来自缓冲区被覆盖或类似情况的事实。现在,您没有崩溃,这意味着利用者有机会在您的代码中执行。


d
djna

您认为返回 null 有哪些替代方法?

我看到两种情况:

查找AnItem(id)。如果找不到该项目该怎么办

在这种情况下,我们可以:返回 Null 或抛出(检查的)异常(或者创建一个项目并返回它)

listItemsMatching (criteria) 如果什么也没找到,应该返回什么?

在这种情况下,我们可以返回 Null、返回空列表或抛出异常。

我相信 return null 可能不如替代方案好,因为它要求客户端记住检查 null,程序员忘记和编码

x = find();
x.getField();  // bang null pointer exception

在 Java 中,抛出检查异常 RecordNotFoundException 允许编译器提醒客户端处理大小写。

我发现返回空列表的搜索非常方便 - 只需使用列表的所有内容填充显示,哦它是空的,代码“正常工作”。


抛出异常以指示在程序正常流程中可能发生的情况会导致非常丑陋的代码,例如 try { x = find() } catch (RecordNotFound e) { // do stuff }。
返回空列表是一个很好的解决方案,但仅当方法位于可以返回列表的上下文中时。对于“findById”情况,您需要返回 null。我不喜欢 RecordNotFound 异常。
这就是我们意见分歧的症结所在。在我看来,x = find(); 之间的美并没有太大区别; if ( x = null ) { work } else { do stuff } 并尝试 catch。而且,如果有必要,我准备牺牲美感来换取代码的正确性。在我的生活中,我经常遇到没有检查返回值的代码。
D
David Plumpton

让他们在事后调用另一个方法来确定之前的调用是否为空。 ;-) 嘿,它是 good enough for JDBC


D
Denis Biondic

好吧,这肯定取决于方法的目的……有时,更好的选择是抛出异常。这一切都取决于具体情况。


V
Vatine

有时,返回 NULL 是正确的做法,但特别是当你处理不同类型的序列(数组、列表、字符串、你有什么)时,返回一个零长度的序列可能更好,因为它导致更短且希望更易于理解的代码,同时在 API 实现者方面不需要更多的编写。


G
GregJF

该线程背后的基本思想是防御性编程。也就是说,针对意外情况编写代码。有一系列不同的回复:

Adamski 建议查看 Null Object Pattern,该回复被投票支持该建议。

Michael Valenty 还建议使用命名约定来告诉开发人员可能会发生什么。 ZeroConcept 建议正确使用 Exception,如果这是 NULL 的原因。和别的。

如果我们制定了我们总是想做防御性编程的“规则”,那么我们可以看到这些建议是有效的。

但是我们有两个开发场景。

由开发人员“编写”的类:作者

另一个(可能)开发人员“使用”的类:开发人员

无论一个类是否为具有返回值的方法返回 NULL,开发人员都需要测试该对象是否有效。

如果开发人员无法做到这一点,那么该类/方法就不是确定性的。也就是说,如果获取对象的“方法调用”没有做它“宣传”的事情(例如getEmployee),它就违反了合同。

作为课程的作者,我总是希望在创建方法时保持善良和防御性(和确定性)。

因此,鉴于需要检查 NULL 或 NULL OBJECT(例如 if(employee as NullEmployee.ISVALID))并且这可能需要在员工集合中发生,那么 null 对象方法是更好的方法。

但我也喜欢 Michael Valenty 关于命名必须返回 null 的方法的建议,例如 getEmployeeOrNull。

抛出异常的作者正在取消开发人员测试对象有效性的选择,这对对象集合非常不利,并在分支其消费代码时强制开发人员进行异常处理。

作为使用该类的开发人员,我希望作者让我能够避免或针对他们的类/方法可能面临的 null 情况进行编程。

因此,作为开发人员,我会针对方法中的 NULL 进行防御性编程。如果作者给了我一个始终返回对象的合同(NULL OBJECT 总是返回)并且该对象具有用于测试对象有效性的方法/属性,那么我将使用该方法/属性继续使用该对象,否则该对象无效,我无法使用它。

底线是类/方法的作者必须提供开发人员可以在其防御性编程中使用的机制。也就是说,该方法的意图更明确。

开发人员应始终使用防御性编程来测试从另一个类/方法返回的对象的有效性。

问候

格雷格


M
Marcin Deptuła

其他选项包括:返回一些指示成功与否(或错误类型)的值,但如果您只需要指示成功/失败的布尔值,则返回 null 表示失败,而返回一个对象表示成功不会不太正确,然后返回真/假并通过参数获取对象。其他方法会使用异常来指示失败,但在这里 - 实际上有更多的声音,说这是一种不好的做法(因为使用异常可能很方便,但有很多缺点)。因此,我个人认为返回 null 表示出现问题并稍后检查(实际知道您是否成功)并没有什么不好的地方。此外,盲目地认为您的方法不会返回 NULL,然后将您的代码基于它,可能会导致其他有时很难找到的错误(尽管在大多数情况下它只会使您的系统崩溃:),正如您将参考0x00000000 迟早)。


C
Community

在复杂程序的开发过程中可能会出现意外的空函数,就像死代码一样,这种情况表明程序结构存在严重缺陷。空函数或方法通常用作对象框架中可重新向量函数或可覆盖方法的默认行为。

Null_function @wikipedia


A
Anon

如果代码是这样的:

command = get_something_to_do()

if command:  # if not Null
    command.execute()

如果您有一个虚拟对象,其 execute() 方法什么都不做,并且在适当的情况下返回它而不是 Null,则您不必检查 Null 情况,而只需执行以下操作:

get_something_to_do().execute()

因此,这里的问题不在于检查 NULL 与异常之间,而是在于调用者必须以不同方式(以任何方式)处理特殊的非情况。


A
Aman Gupta

对于我的用例,我需要从方法返回一个 Map,然后寻找一个特定的键。但是,如果我返回一个空 Map,那么它会导致 NullPointerException,然后返回 null 而不是一个空 Map 不会有太大不同。但从 Java8 开始,我们可以使用 Optional。以上就是引入 Optional 概念的原因。


R
Rob Wells

天,

当您无法创建新对象时返回 NULL 是许多 API 的标准做法。

为什么这是糟糕的设计,我不知道。

编辑:这适用于没有例外的语言,例如多年来一直是惯例的 C。

高温高压

'很高兴,


如果你不能创建一个对象,你真的应该抛出一个异常。如果您无法找到与某个查询匹配的对象,则返回 null 是要走的路。
@Thilo,告诉我如何在 C 中做到这一点,我会最感兴趣。
@RobWells C 不是面向对象的语言,但这个问题被标记为“oop”
@yegor256 你是对的。我错过了原来的 OOP 标签。但是,正如 BS 所说,C++ 中的类只是一个结构,添加了一些额外的成员函数,并在内部进行了一些花哨的内存管理。但是如果一个 API 应该返回一个结构,那么当你不能创建结构时返回 NUL 通常是惯例。