返回无效的不良设计吗?[关闭]


127

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

伪代码:

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

13
详细说明:这些说不好的人在哪里?链接?
jcollum

2
如果该方法是您可以控制的方法,则可以进行单元测试以确保它永远不会返回null,否则,我不明白为什么在调用之后检查它是否为null是一种不好的做法;在该方法上返回null可能是一个坏习惯,但是您必须保护您的代码
BlackTigerX 2009年

9
仅由于没有要返回的数据而引发异常非常令人讨厌。正常的程序流程不应引发异常。
Thorarin

4
@David:那是我真正说的话。如果一个方法应该返回数据,但没有返回数据,则意味着出了点问题。那不是正常的程序流程:)
Thorarin

2
@Thorarin:“正常”程序流程是一个可扩展的概念:并不是一个可靠的论点基础。
Tomislav Nakic-Alfirevic

Answers:


206

不返回null的基本原理是您不必检查它,因此您的代码无需根据返回值遵循其他路径。您可能想查看Null对象模式,它提供了有关此信息的更多信息。

例如,如果我要用Java定义一个返回Collection的方法,那么我通常希望返回一个空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.
  }
}

6
是的,比返回null并希望客户记得处理此案更好。
2009年

10
我会很高兴地同意,当将null用作空容器(或字符串)的替代品时,返回null是疯狂的。但是,这不是常见的情况。
MSalters

2
我也为Null Object Pattern +1。同样,当我实际上想返回null时,我一直在命名诸如getCustomerOrNull()之类的方法以使其明确。我认为当读者不想看实现时,一种方法的命名就很好。
2009年

4
注释“ //现在有两个可能的代码路径”不正确;无论哪种方式,您都有两个代码路径。但是,在第一个示例中使用null Collection对象时,“ null”代码路径会更短。但是,您仍然有两条路径,并且仍然需要测试两条路径。
Frerich Raabe,2009年

1
@MSalters- OO语言中的每个变量都可以带有null一个“容器”,其中包含指向对象的零个或一个指针。(这肯定是Haskell Maybe在这些情况下明确建模的方式,这样做更好。)
Andrzej Doyle

71

这就是原因。

他在Robert Martin的Clean Code中写道,如果可以改为返回空数组,则返回null是错误的设计。由于预期结果是一个数组,为什么不呢?它使您无需任何额外条件即可遍历结果。如果是整数,则可能为0,如果是哈希,则为空哈希。等等

前提是不要强迫调用代码立即处理问题。调用代码可能不想与它们有关。这就是为什么在许多情况下,例外比零更好。


2
这是在这个答案中提到的空对象模式:stackoverflow.com/questions/1274792/...
斯科特多尔曼

这里的想法是,出错时,您将返回通常返回的任何对象类型的空/空白版本。例如,在这种情况下,可以使用空数组或字符串。当您通常返回指针时,返回“ NULL”是适当的(因为NULL本质上是一个空指针)。如果从通常返回哈希的函数返回错误,则返回NULL可能会造成混淆。有些语言比其他语言处理得更好,但总的来说,一致性是最佳实践。
bta 2010年

您不一定会返回错误,除非错误以某种方式控制流(这不是一个好习惯)或在API或接口中抽象出来。错误在那里传播到您决定捕获它的任何级别,因此您不必在调用上下文中对其进行处理。默认情况下,它们是null-object-pattern-friendly。
Max Chernyak 2010年

不幸的是,调用代码并不总是考虑空集合的情况(就像它不考虑空值情况)。这可能是问题,也可能不是问题。
Sridhar Sarnobat

38

返回null的好用法:

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

不良用法:尝试替换或隐藏特殊情况,例如:

  • catch(...)并返回null
  • API依赖项初始化失败
  • 磁盘空间不足
  • 输入参数无效(编程错误,输入必须由调用方清除)
  • 等等

在这些情况下,抛出异常就足够了,因为:

  • 返回值为空不会提供有意义的错误信息
  • 直接呼叫者很可能无法处理错误情况
  • 无法保证调用方检查的是空结果

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

  • 无效的用户名/密码(或任何用户提供的输入)
  • 中断循环或非本地异常

7
但是,“无效的用户名”似乎是引发异常的一个很好的原因。
Thilo

11
用户输入无效的登录名/密码不应视为特殊情况,这是正常程序操作的一部分。但是,身份验证系统无法响应(例如,活动目录)是一种例外情况。
ZeroConcept


2
我会说这取决于:boolean login(String,String)看起来不错,也是这样AuthenticationContext createAuthContext(String,String) throws AuthenticationException
sfussenegger 2010年

1
在您的示例中,如果没有第一个需要处理的对象,则有两种合理的方式来处理这种情况。第一个是Null Object模式。创建一个最终的子类,该子类表示该类的不存在版本。它具有合理的默认值,并在请求废话动作时引发异常。另一种技术是Option。这是Java8和Scala中可用的功能。这些都表明开发人员的意图。null不能显示意图,因为它具有太多可能的含义。**显示意图**是代码最重要的方面。
斯科特·加德纳2014年

29

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

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

查看此博客文章以获取详细说明:http : //www.yegor256.com/2014/05/13/why-null-is-bad.html。在我的《优雅的物体》第4.1节中有更多内容。


2
我非常同意。我什至不喜欢空对象模式,这似乎是一种“粉饰”延迟策略。您的方法应该总是成功,或者不一定成功。如果应该总是成功,则抛出。如果可能不会成功,然后设计方法让消费者知道,例如bool TryGetBlah(out blah)FirstOrNull()MatchOrFallback(T fallbackValue)
路加·普普利特

我也不喜欢返回null。您正在将根本原因与症状分开,从而使调试更加困难。抛出异常(快速失败),或调用首先返回布尔值的checker方法(例如isEmpty()),并且只有在返回true时才调用该方法。人们反对第二个说法,那就是它的性能较差-但正如Unix Philosophy所说,“将人的时间与机器时间相提并论”(即,性能的降低比开发人员调试产生虚假错误的代码浪费的时间更少)。
Sridhar Sarnobat

22

谁说这是不好的设计?

检查空值是一种惯例,甚至鼓励这样做,否则,到处都有NullReferenceExceptions的风险。与不需要时抛出异常相比,优雅地处理错误要好于抛出异常。


11
+1。对于可以立即缓解的问题抛出异常的代码,令我感到难过。
jkeys

6
当编码人员忘记检查空值然后获取神秘的空指针异常时,您会感到多么悲伤?检查异常时,编译器会提醒用户他们没有处理错误情况,从而避免了编码错误。
djna

3
@djna:我想这也很可悲,但是我发现,“忘记”检查空值的相同类型的编码器是那些在处理受检查的异常时经常吞下它们的编码器。
兰迪

5
发现空的catch块比没有空检查要容易。
普雷斯顿,

20
@普雷斯顿:我强烈不同意。如果没有空检查,当它崩溃时,您会立即发现它。吞下的异常可能会传播多年的神秘和微妙的错误……
Beska

18

根据您到目前为止的发言,我认为没有足够的信息。

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

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


4
类似于我的约定:单项查询- Create...返回一个新实例,或throwsGet...返回一个预期的现有实例,或者抛出GetOrCreate...返回一个现有实例,如果不存在则返回一个新实例,否则抛出Find...返回一个现有实例(如果存在),null对于集合查询- Get...始终返回一个集合,如果找不到匹配项,则为空。
Johann Gerell 2014年

由于yegor256和hakuinin的答案中给出的原因,NULL不好,返回有效但空的对象可简化推理过程
Rob11311,2016年


10

这取决于您使用的语言。如果您使用的是C#之类的语言,表示缺少值的惯用方式是返回null,那么如果没有值,则返回null是一个不错的设计。另外,在像Haskell这样的语言中,在这种情况下习惯使用Maybe monad的语言,那么返回null将是一个糟糕的设计(即使可能的话)。


2
+1可能代表单子。我发现nullC#和Java 之类的语言中的定义经常被重载,并在域中赋予了一定的意义。如果您null在语言规范中查询关键字,则仅表示“无效指针”。在任何问题领域,这可能毫无意义。
MattDavey 2014年

问题是您不知道它是“缺失值” null还是“未初始化”null
CervEd

5

如果您阅读了所有答案,则可以很清楚地看到此问题的答案取决于方法的类型。

首先,当发生异常情况(IO问题等)时,逻辑上会引发异常。当确实有什么例外的时候,可能是针对其他主题的事情。

只要期望一种方法可能没有结果,就会有两类:

  • 如果有可能返回中性值,请这样做
    空的枚举,字符串等都是很好的例子
  • 如果不存在这样的中性值,则应返回null
    如前所述,假定该方法可能没有结果,因此它不是例外,因此不应引发异常。中性值是不可能的(例如:0并不是特别中性的结果,具体取决于程序)

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

我还不太满意这个名字,但是无法提出更好的建议。所以我现在就开始讨论。


4

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

对于单项查询:

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

对于集合查询:

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

[1]在函数名称或参数中给出了一些显式或隐式条件。


如果找不到,为什么GetOne和FindOne返回不同​​?
Vladimir Vukanac

我描述了差异。我使用Get希望它在那里的时间,所以如果它不在那里,那就是一个错误,然后我抛出-我永远不需要检查返回值。Find如果我真的不知道它是否存在,我会使用-然后我需要检查返回值。
Johann Gerell '18

3

例外是针对例外情况。

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


1
如果您尝试使用没有值的属性,那将是一个例外。(我假设您的规范说该属性是可选的。)有一个单独的方法来检查其值是否已设置。
普雷斯顿,

3

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

public String getEmployeeName(int id){ ..}

在这种情况下,如果id与现有实体不对应,则返回null很有用,因为它可以让您区分出从合法错误中找不到匹配项的情况。

人们可能会认为这很糟糕,因为它可能会被滥用为表示错误状态的“特殊”返回值,该返回值并不是很好,有点像从函数返回错误代码,但由于用户必须检查返回值而感到困惑null,而不是捕获适当的异常,例如

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

3
对。如果遇到错误情况,请抛出异常。
David Thornley,2009年

3
在这种情况下,将需要例外。如果您请求的雇员姓名不存在,那么显然出了问题。
Thorarin

我认为这有点字面意思,Thorarin-当然可以对我的示例进行询问,但是我敢肯定,您可以想象到某个函数实例可以返回匹配的值,如果没有匹配,则返回null。如何从哈希表中的键获取值?(本来应该想到该示例)。
史蒂夫·B

1
散列表为不存在的键返回null是不好的。自动执行此操作意味着在没有不一致的代码的情况下,您不能将null存储为值。
RHSeeger

3

它不一定是不良的设计-取决于许多设计决策,这取决于设计。

如果该方法的结果不能正常使用,则返回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 /* ... */ {
}

2
尽管您可以在此处记录诊断错误,但是也许可以重新抛出异常。有时,首次故障数据捕获会非常有帮助。
djna

或者将异常包装在RuntimeException中,并在消息字符串中包含诊断信息。将信息保持在一起。
托尔比约恩Ravn的安徒生

现在(在2018年)我不再同意,在这种情况下,我们应该支持返回Optional<>
PiotrMüller18年

2

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

检查NULL并在失败情况下不声明(对于程序员错误)或抛出(对于用户或调用者错误)可能意味着以后的崩溃更难追踪,因为未找到原始的奇数情况。

此外,忽略错误可能导致安全漏洞。无效性可能来自缓冲区被覆盖之类的事实。现在,您不会崩溃,这意味着开发者有机会在您的代码中执行。


2

您看到返回null的其他选择吗?

我看到两种情况:

  • findAnItem(id)。如果找不到该怎么办

在这种情况下,我们可以:返回Null或引发一个(已检查的)异常(或者可以创建一个项目并将其返回)

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

在这种情况下,我们可以返回Null,返回空列表或引发Exception。

我认为,返回null可能不如其他方法好,因为它要求客户端记住检查null,程序员忘记并编写代码

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

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

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


3
抛出异常以指示预期在程序正常流程期间可能发生的条件会导致代码非常丑陋,例如try {x = find()} catch(RecordNotFound e){//做东西}。
–quant_dev

返回空列表是一个很好的解决方案,但是仅当该方法位于可以返回列表的上下文中时。对于“ findById”情况,您需要返回null。我不喜欢RecordNotFound异常。
Thilo

因此,这就是我们意见分歧的症结所在。在我看来,x = find()之间的美感并没有太大区别。if(x = null){工作} else {做事}并尝试catch。而且,如有必要,我准备为代码正确性而牺牲美观。我一生中经常遇到未检查返回值的代码。
09年

1

有时候,返回NULL是正确的做法,但是特别是当您处理不同种类的序列(数组,列表,字符串,所用的东西)时,最好返回零长度的序列,因为导致代码更短,并且希望可以更容易理解,而无需在API实现者方面花费更多的时间。



1

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

Adamski建议查看“空对象模式”,并对该建议投赞成票。

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

如果我们做出我们一直想进行防御性编程的“规则”,那么我们可以看到这些建议是有效的。

但是我们有2个开发方案。

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

由另一个(也许)开发人员“消耗”的类:开发人员

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

如果开发人员无法执行此操作,则该类/方法不确定。也就是说,如果获取对象的“方法调用”没有执行其“通告”的操作(例如getEmployee),则它违反了合同。

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

因此,鉴于需要检查NULL或NULL OBJECT(例如if(employee as NullEmployee.ISVALID)),并且可能需要对一组Employees进行检查,因此空对象方法是更好的方法。

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

引发异常的作者正在删除开发人员测试对象有效性的选择,这对一组对象非常不利,并在分支其使用的代码时强制开发人员进行异常处理。

作为开发人员,我希望作者能够给我提供避免或编程他们的类/方法可能遇到的无效情况的能力。

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

最重要的是,类/方法的作者必须提供开发人员可以在防御性编程中使用的机制。即,该方法的意图更明确。

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

问候

格雷格·JF


0

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


0

在复杂程序的开发过程中可能会出现意外的空函数,并且像死代码一样,这种现象表明程序结构中存在严重缺陷。

空函数或方法通常用作对象框架中可重新引导函数或可重写方法的默认行为。

Null_function @维基百科



0

如果代码是这样的:

command = get_something_to_do()

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

如果您有一个虚拟对象,其execute()方法不执行任何操作,并且在适当的情况下返回该对象而不是Null,则无需检查Null情况,而可以执行以下操作:

get_something_to_do().execute()

因此,这里的问题不在检查NULL与检查异常之间,而是在调用者不得不以不同的方式(无论以何种方式)处理特殊的不区分大小写之间。


0

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


-3

G'day,

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

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

编辑:对于没有例外的语言,例如C,多年来的惯例,这都是正确的。

高温超导

'Avahappy,


2
如果无法创建对象,则应真正抛出异常。如果找不到与某些查询匹配的对象,则返回null是可行的方法。
Thilo

@Thilo,告诉我如何在C中做到这一点,我将最感兴趣。
罗布·威尔斯

@RobWells C不是一种面向对象的语言,但此问题被标记为“ oop”
yegor256

@ yegor256你是对的。我错过了原始的OOP标签。但是,正如BS所说,C ++中的类只是一个结构,其中添加了一些其他成员函数,并且内部进行了一些高级内存管理。但是,如果应该使用API​​返回结构,则通常无法在无法创建结构的情况下返回NUL。
罗伯·威尔斯
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.