从不存在返回值的函数/方法中返回NULL或空值更好吗?


135

我在这里寻找建议。我正在努力在不存在返回值或无法确定返回值的情况下从方法返回NULL还是返回空值更好。

以以下两种方法为例:

string ReverseString(string stringToReverse) // takes a string and reverses it.
Person FindPerson(int personID)    // finds a Person with a matching personID.

在中ReverseString(),我会说返回一个空字符串,因为返回类型是字符串,因此调用者期望得到。同样,通过这种方式,调用方将不必检查是否返回了NULL。

在中FindPerson(),返回NULL似乎更合适。无论是否new Person()返回NULL或空的Person Object(),调用者都必须在对其进行任何操作(如调用UpdateName())之前检查一下Person对象是否为NULL或为空。因此,为什么不只在此处返回NULL,然后调用方只需检查NULL。

还有其他人为此感到挣扎吗?任何帮助或见解表示赞赏。


2
会有所不同。您也可能会争辩说,该方法应停止预先不正确的值,例如,您的stringToReverse参数作为空或null传入。如果执行了该检查(抛出异常),它将始终返回非null的值。
亚伦·麦克弗

福勒·波伊亚-p。496种基本模式-特殊情况(空对象)Bloch Effective Java,第二版项目43:返回空数组或集合,而不是null
Boris Treukhov 2011年

1
@鲍里斯,我没看到你的评论。我实际上只是发布了“特例”作为答案。
Thomas Owens

@Thomas我没有发现任何问题:)。至于这个问题,无论如何都是常识。
鲍里斯·特鲁霍夫

一个有趣的评论是,在Oracle数据库中,NULL和空字符串是相同的
WuHoUnited 2011年

Answers:


77

在本问答中, StackOverflow对这个确切的主题进行了很好的讨论。在评分最高的问题中,克罗诺兹指出:

如果您打算指示没有可用数据,则返回null通常是最好的主意。

空对象表示已返回数据,而返回null则表示未返回任何内容。

此外,如果您尝试访问对象中的成员,则返回null将导致null异常,这对于突出显示错误代码很有用-尝试访问无成员的成员没有任何意义。访问空对象的成员不会失败,这意味着错误可能会被发现。

就我个人而言,我喜欢为返回字符串的函数返回空字符串,以最大程度地减少需要放置的错误处理量。但是,您需要确保与您一起工作的小组遵循相同的约定-否则将无法获得此决定的好处。

但是,正如SO答复中指出的那样,如果期望对象,则应该返回null,这样就毫无疑问是否要返回数据。

最后,没有唯一的最佳方法。建立团队共识将最终推动团队的最佳实践。


8
就我个人而言,我喜欢为返回字符串的函数返回空字符串,以最大程度地减少需要放置的错误处理量。从不理解这个论点。空检查是什么,最多<10个字符?为什么我们这样行事呢?
亚伦·麦克弗

19
没有人说这是回力劳动。但这劳动。它在代码中添加了一种特殊情况,这意味着要再测试一个分支。另外,它破坏了代码流。for x in list_of_things() {...}可以说是更快了l = list_of_things(); if l != null {...}
Bryan Oakley

5
@Bryan:空值列表和空字符串之间有很大区别。字符串很少代表字符列表。
凯文·克莱恩

3
@kevin克莱恩:当然。但是对于字符串,无论是否为空,您都可能希望该字符串出现在日志中。必须检查它是否为空,以便您可以打印一个空字符串来混淆真正的意图。或类似于数据库字段,其中包含您要添加到网页的用户提供的内容。emit(user.hometown)比做起来容易得多if user.hometown == null { emit("") else {emit(user.hometown)}
Bryan Oakley

1
它不仅是多余的输入(不是很多),而且会影响可读性。我个人发现带有一堆空检查的代码使人分心。
ToddR 2011年

82

在我编写的所有代码中,都避免null从函数返回。我在“ 干净代码”中阅读过该内容。

使用的问题null在于,使用接口的人员不知道是否null可能产生结果,以及是否必须检查它,因为没有not null引用类型。

在F#中,您可以返回一个option类型,可以是some(Person)none,因此对于调用者来说,他们必须检查它是显而易见的。

类似的C#(反)模式是以下Try...方法:

public bool TryFindPerson(int personId, out Person result);

现在我知道人们已经说过他们讨厌这种Try...模式,因为拥有输出参数会破坏纯函数的思想,但实际上并没有什么不同:

class FindResult<T>
{
   public FindResult(bool found, T result)
   {
       this.Found = found;
       this.Result = result;
   }

   public bool Found { get; private set; }
   // Only valid if Found is true
   public T Result { get; private set;
}

public FindResult<Person> FindPerson(int personId);

...而且说实话,您可以假设每个 .NET程序员都知道该Try...模式,因为.NET框架在内部使用了该模式。这意味着他们不必阅读文档即可了解其功能,这对我来说比坚持一些纯粹主义者的功能观点(了解这result是一个out参数,而不是ref参数)更为重要。

因此,我同意,TryFindPerson因为您似乎表明无法找到它是完全正常的。

另一方面,如果没有逻辑上的原因,调用者会提供一个personId不存在的,我可能会这样做:

public Person GetPerson(int personId);

...然后如果异常无效,我将引发异常。该Get...前缀意味着调用者知道它应该成功。


28
+1,如果您尚未这样做,我会参考清洁代码来回答这个问题。我喜欢这句口头禅:“如果您的功能不能说出它的名字,那就抛出例外。” Muuuch比检查每一个十几行空左右为佳....
格雷厄姆

13
我已经放弃了任何有关人的知识的假设。
CaffGeek

5
“使用null的问题在于使用接口的人不知道null是否可能是结果,以及他们是否必须检查它,因为没有不为null的引用类型。” 由于引用类型可以为null,因此您是否不能始终假设null可能是结果?
汤米·卡利尔

4
如果一个函数返回一个指针(C / C ++)或对一个对象的引用(Java),我总是认为它可能返回null。
乔治

7
“使用null的问题是使用接口的人不知道null是否是可能的结果,以及他们是否必须检查它,[...]”如果某个方法可能返回null,则应明确作为API合同的一部分提及。
ZsoltTörök

28

您可以从企业应用程序体系结构的模式中尝试Martin Fowler的特例模式

空值在面向对象的程序中很尴尬,因为它们会击败多态性。通常,您可以在给定类型的变量引用上自由调用foo,而不必担心该项是确切类型还是子类。使用强类型语言,您甚至可以让编译器检查调用是否正确。但是,由于变量可以包含null,因此通过在null上调用一条消息可能会遇到运行时错误,这将为您提供友好的堆栈跟踪。

如果变量可能为null,则必须记住用null测试代码将其括起来,以便在存在null的情况下做正确的事情。通常在很多情况下正确的事情都是相同的,因此您最终会在很多地方编写类似的代码-犯下了代码重复的罪过。

空值是此类问题的常见示例,其他问题经常出现。在数字系统中,您必须处理无穷大,它对加法等事物具有特殊的规则,这些规则打破了实数通常的不变性。我在商业软件方面的最早经验之一是与一位鲜为人知的公用事业客户联系,该客户被称为“乘员”。所有这些暗示着改变了该类型的通常行为。

而不是返回null或某个奇数,而是返回一种特殊情况,该特殊情况具有与调用者期望的接口相同的接口。


我遇到了这种情况,但是只是在软件发布之后。我的测试数据从未触发过它,所以它有点出乎意料,调试起来很麻烦。尽管我没有用“特殊情况”模式解决它,但是很高兴意识到这一点。
samis

14

我认为ReverseString()会返回反转的字符串,IllegalArgumentException如果传入则会抛出一个Null

我认为FindPerson()应该遵循NullObject模式,或者如果您始终能够找到某物,则引发有关未找到某物的未经检查的异常。

不必处理Null情况是应该避免的。有Null在语言被称为十亿美元的错误由它的发明者!

我称之为我的十亿美元错误。这是在1965年发明空引用的。那时,我正在设计第一个全面的类型系统,用于面向对象语言(ALGOL W)的引用。

我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但是我忍不住要插入一个空引用的诱惑,仅仅是因为它是如此容易实现。

这导致了无数的错误,漏洞和系统崩溃,在最近四十年中可能造成十亿美元的痛苦和破坏。

近年来,许多程序分析器(例如Microsoft的PREfix和PREfast)已用于检查引用,并在有可能不为空的风险时发出警告。诸如Spec#之类的最新编程语言引入了非空引用的声明。这是解决方案,我在1965年拒绝了。

托尼·霍尔


11

返回一个Option。返回不同的无效值的所有好处(例如能够在集合中包含空值)而没有NullPointerException的风险。


1
有趣。如何在Java中实现选项?在C ++中?
乔治

1
@Giorgio,对于Java,来自Google的Guava库具有一个名为Optional的类。
Otavio Macedo

1
对于C ++,有boost::optional<T>
MSalters

8

我看到了这个论点的两面,并且我意识到一些颇具影响力的声音(例如Fowler)主张不返回空值以保持代码干净,避免额外的错误处理块等。

但是,我倾向于支持返回null的支持。我发现在调用方法时有一个重要的区别,它在响应时没有任何数据,而在响应时却有一个空String

由于我已经看到了一些有关Person类的讨论,因此请考虑尝试查找该类实例的情况。如果您传递某些查找程序属性(例如,ID),则客户端可以立即检查是否为空以查看是否未找到任何值。这不一定是例外(因此不需要例外),但也应明确记录在案。是的,这需要客户方面的一些严格要求,不,我认为这根本不是一件坏事。

现在考虑替代方案,在该方案中返回一个没有任何内容的有效Person对象。您是在其所有值(名称,地址,favouriteDrink)中放置空值,还是现在使用有效但空的对象填充空值?您的客户现在如何确定未找到实际的人?他们是否需要检查名称是否为空字符串而不是null?难道这种事情实际上不会导致比我们只检查null并继续前进的情况更多或更多的代码混乱和条件语句吗?

再说一次,我可以同意这个论点的两面,但是我发现这对大多数人来说是最有意义的(使代码更具可维护性)。


区别是,“ 如果键不存在,是返回FindPerson还是GetPerson返回null或引发异常?” 作为API的用户,我只是不知道,我必须检查文档。另一方面,bool TryGetPerson(Guid key, out Person person)不需要我检查文档,并且我知道在相当非异常的情况下不会引发异常。这就是我要在回答中指出的重点。
斯科特·惠特洛克

4
我认为人们太喜欢例外了。异常是不好的并且消耗资源。更不用说人们确实在不解决任何问题的情况下尝试捕获语句。异常是明显的迹象,表明根本上出错了。例如,空引用会引发异常,因为引用空对象显然是错误的。用它来检查是否有人发现是错误的。
弗拉基米尔·科恰琴奇(Fladimir Kocjancic),2014年

1
@VladimirKocjancic:我完全同意:例外情况应用于例外情况,例如软件错误,硬件故障,系统配置错误。如果您要计算部分函数(如findPerson),则无结果绝对是正常的。这不应导致异常。
Giorgio

6

如果您需要知道该项目是否存在,则返回null。否则,返回期望的数据类型。如果要返回项目列表,则尤其如此。通常可以安全地假设呼叫者想要一个列表,而他们想要遍历该列表。如果您尝试遍历null而不是遍历一个空列表,则许多(大多数?全部?)语言会失败。

我发现尝试使用这样的代码令人沮丧,只是让它失败了:

for thing in get_list_of_things() {
    do_something_clever(thing)
}

没有东西的情况我不必加一个特殊情况。


7
正是由于这个原因,在期望使用列表或其他值组的情况下,我总是选择返回一个空列表。默认操作(在一个空列表中重复)几乎总是正确的,如果不是,则调用方可以显式检查该列表是否为空。
TMN

5

它取决于方法的语义。您可以指定,您的方法仅接受“ Not null”。在Java中,您可以使用一些元数据注释来声明:

string ReverseString(@NotNull String stringToReverse)

您应该以某种方式指定返回值。如果声明,则仅接受NotNull值,则必须返回空字符串(如果输入也是空字符串)。

第二种情况在语义上有点复杂。如果此方法是通过主键返回某个人(并且该人应该存在),则更好的方法是引发异常。

Person FindPerson(int personID)

如果您通过某个猜测的ID搜索人(在我看来这很奇怪),则最好将该方法声明为@Nullable。


3

我建议尽可能使用Null Object模式。它简化了调用方法时的代码,并且您不会阅读像

if (someObject != null && someObject.someMethod () != whateverValue)

例如,如果一个方法返回一个适合某种模式的对象的集合,那么返回一个空集合要比nullreturn 有意义,并且在这个空集合上进行迭代实际上不会造成性能损失。另一种情况是一种方法,该方法返回用于记录数据的类实例,返回一个Null对象,而不是null我认为更可取的方法,因为它不会强制用户始终检查返回的引用是否为null

如果返回null是有意义的(findPerson ()例如,调用该方法),则我将尝试至少提供一种返回(如果存在该对象的话)的方法(personExists (int personId)例如),另一个示例是Java中的containsKey ()方法Map。它使调用者的代码更整洁,因为您可以很容易地看到所需的对象可能不可用(人员不存在,映射中不存在键)。null在我看来,不断检查引用是否使代码模糊不清。


3

当且仅当满足以下条件时,null是最好的返回值:

  • 在正常操作中预期为空结果。在某些合理的情况下,可能无法找到一个人,所以findPerson()返回null是可以的。但是,如果确实是意外失败(例如,在名为saveMyImportantData()的函数中),则应该抛出异常。名称中有一个提示-例外是在特殊情况下!
  • null表示“未找到/没有值”。如果您还有其他意思,请返回其他内容!很好的例子是返回Infinity或NaN的浮点运算-这些是具有特定含义的值,因此,如果您在此处返回null,将变得非常混乱。
  • 该函数旨在返回单个值,例如findPerson()。如果它被设计为返回一个集合,例如findAllOldPeople(),那么最好使用一个空集合。结果是,返回集合的函数永远不要返回null

此外,请确保您记录了该函数可以返回null 的事实

如果遵循这些规则,则null几乎是无害的。请注意,如果您忘记检查是否为null,通常通常会在此后立即获得NullPointerException,这通常是一个很容易修复的错误。这种快速失败的方法比具有伪造的返回值(例如,空字符串)要好得多,该伪造的返回值在系统中悄悄传播,可能破坏数据而不会引发异常。

最后,如果将这些规则应用于问题中列出的两个功能:

  • FindPerson-如果找不到此人,此函数将返回null
  • ReverseString-似乎通过传递字符串永远不会失败(因为所有字符串都可以颠倒,包括空字符串)。因此,它永远不应返回null。如果出现问题(内存不足?),则应引发异常。

1

在我看来,返回NULL,返回一些空结果(例如,空字符串或空列表)和引发异常之间是有区别的。

我通常采用以下方法。我认为函数f(v1,...,vn)的调用是函数的应用

f : S x T1 x ... x Tn -> T

其中,S是“世界状态” T1,...,Tn是输入参数的类型,T是返回类型。

我首先尝试定义此功能。如果该函数是部分函数(即某些输入值未定义),则返回NULL表示此。这是因为我希望计算正常终止,并告诉我所请求的功能未在给定的输入上定义。例如,使用空字符串作为返回值是模棱两可的,因为可能是在输入上定义了函数,并且空字符串是正确的结果。

我认为在调用代码中需要对NULL指针进行额外检查,因为您正在应用部分函数,​​并且被调用方法的任务是告诉您是否未为给定输入定义该函数。

我更喜欢将异常用于不允许执行计算的错误(即无法找到任何答案)。

例如,假设我有一个Customer类,并且想实现一个方法

Customer findCustomer(String customerCode)

通过其代码在应用程序数据库中搜索客户。用这种方法,我会

  • 如果查询成功,则返回Customer类的对象,
  • 如果查询找不到任何客户,则返回null。
  • 如果无法连接到数据库,则引发异常。

额外检查是否为空,例如

Customer customer = findCustomer("...");
if (customer != null && customer.getOrders() > 0)
{
    ...
}

是我正在做的工作的语义的一部分,我不会只是“跳过它们”以使代码读起来更好。我认为简化当前问题的语义只是为了简化代码不是一个好习惯。

当然,由于对null的检查非常频繁,因此如果该语言支持某些特殊语法,则很好。

只要我可以将类的空对象与所有其他对象区分开,我也将考虑使用空对象模式(由Laf建议)。


0

返回集合的方法应返回空,而其他方法则可以返回null,因为对于这些集合,可能会发生这样的情况,即您将拥有对象,但其中没有元素,因此调用方将仅针对大小而不是对两者进行验证。


0

对我来说,这里有两种情况。如果要返回某种列表,则无论如何都应始终返回列表;如果没有任何内容,则为空。

我唯一有争议的情况是您退货时。我发现自己更喜欢在使事情快速失败的基础上在失败的情况下返回null。

如果返回空对象,并且调用方需要一个真实对象,则他们可能会继续尝试使用该空对象产生意外行为。我认为,如果他们忘记处理这种情况,则常规行事会更好。如果出了问题,我希望尽快处理异常。您以这种方式发布错误的可能性较小。


0

人们已经对Maybe类型构造函数说了些什么:

除了不冒NPE的风险外,另一个重大收获是语义上的收获。在函数世界中,我们处理纯函数,我们希望尝试使函数在应用时与返回值完全相等。考虑以下情况String.reverse():这是一个给定特定字符串表示该字符串反向版本的函数。我们知道该字符串存在,因为每个字符串都可以颠倒(字符串基本上是有序的字符集)。

现在findCustomer(int id)呢?此功能代表具有给定ID的客户。这可能存在或可能不存在。但是请记住,函数具有单个返回值,因此您不能真正说“此函数返回具有给定ID的客户,或者它返回” null

这是我与返回null和返回null对象有关的主要问题。他们具有误导性。null不是客户,也不是“没有客户”。没有任何东西。它只是无类型的指向NOTHING的指针。非常低级,非常丑陋且非常容易出错。我认为,空对象在这里也很容易引起误解。空客户就是客户。不是一个人缺席,也不是要求提供ID的客户,因此返回它就是错误的。这不是我要的客户,但您仍然声称自己回了客户。它具有错误的ID,显然这是一个错误。它破坏了方法名称和签名所建议的契约。它需要客户端代码来承担有关您的设计的事情,或阅读文档。

这两个问题都可以通过解决Maybe Customer。突然,我们不是在说“此功能代表具有给定ID的客户”。我们说的是“此功能代表具有给定ID的客户(如果存在)。现在它非常清楚地说明其功能。如果您要求具有不存在ID的客户,您将会收到Nothing。这会更好吗?比null?不只是为NPE的风险,但也因为类型Nothing不只是Maybe,它是Maybe Customer,它具有语义。这具体是指“没有一个客户”,这表明这样的客户不存在。

null的另一个问题当然是模棱两可。是您没有找到客户,还是数据库连接错误,还是我不被允许看到该客户?

如果您有几个类似的错误情况要处理,则可以抛出这些版本的异常,或者(并且我更喜欢这样)可以返回Either CustomerLoadError Customer,表示出现问题或返回的客户。它具有的所有优点,Maybe Customer同时还允许您指定可能出现的问题。

我要做的主要事情是捕获函数签名中的约定。不要假设事物,不要依赖转瞬即逝的约定。


0

1)如果函数的语义是它什么也不能返回,则调用者必须对此进行测试,否则他将例如 拿走你的钱,把它给任何人。

2)使函数在语义上总是返回某些东西(或抛出)是一件好事。

使用logger时,如果未定义记录器,则返回不记录的记录器是有意义的。返回集合时,几乎没有任何意义(除非在“足够低的级别”上,集合本身就是数据,而不是其中包含的数据)什么都不返回,因为空集是集合,而不是什么。

以一个为例,我将使用两个函数,一个返回null,第二个抛出,这是“休眠”的方式(获取vs加载,IIRC,但由于代理对象的延迟加载而变得很复杂)。


-1
  • 如果应用程序期望数据可用但该数据不可用,则应返回NULL。例如,如果未找到城市,则基于邮政编码返回CITY的服务应返回null。然后,呼叫者可以决定处理null还是崩溃。

  • 如果有两种可能,则应返回空列表。可用数据或无可用数据。例如,如果人口大于特定数目,则返回CITY的服务。如果没有满足给定条件的数据,则可以返回一个空列表。


-2

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

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

检查此博客文章以获取详细说明:http : //www.yegor256.com/2014/05/13/why-null-is-bad.html

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.