返回空列表真的比返回 null 好吗?- 第二部分






4.92/5 (39投票s)
本文旨在回答这个问题:我们应该从函数返回空列表还是“null”?
引言
欢迎阅读系列文章《返回空列表而不是null
真的更好吗?》的第2部分。第1部分介绍了广受欢迎的建议“返回空列表而不是null
”,该建议被软件行业的领导者广泛接受和支持。我们查看了一个演示其原理的源代码示例。最后,暗示该建议可能需要重新考虑。
在这一部分中,我们将通过查看这两种方法的具体源代码示例并检查它们的优缺点来比较这两种方法(即返回空列表与返回null
)。
第二部分:两种方法的比较
两种相反的方法
软件程序中的许多函数都返回集合(集合、列表、映射、数组、字符串等)。我们感兴趣的问题是:如果没有数据要返回,这样的函数应该返回什么?例如,如果教室中没有孩子,以下方法应该返回什么?
public static List<Child> getChildrenInClassRoom()
我们将比较两种方法
-
返回空集合
我们称之为“避免
null
”方法。 -
返回
null
我们称之为“喜爱
null
”方法。
存在其他替代方法,但我们在此不讨论它们。例如,我们可以使用Optional/Maybe
模式。此模式已在我之前的文章《为什么我们应该喜爱'null'》中讨论过(参见“Optional/Maybe模式”一节)。
此外,在这一部分中,我们不考虑如果函数操作无法正确执行(例如无效输入参数、资源错误等)而抛出异常的情况。
目标是保持简单,只关注返回空集合与返回null
。
对于每种方法,我们将考虑
- 软件可靠性
- 时间和空间要求
- API差异
软件可靠性考虑
本章的目的是回答一个关键问题
这两种方法中,哪一种有助于我们编写更可靠的软件?
换句话说
如果我们选择其中一种方法而不是另一种,我们是否会增加严重错误的风险?
让我们从一个非常简单的例子开始。
列出集合中的元素
假设我们想打印客户的订单列表。我们使用以下方法检索给定客户的所有订单
public static List<ICustomerOrder> getOrdersByCustomer ( String customerID )
要打印列表,我们可以这样编写代码
String customerID = "123";
for ( ICustomerOrder order : getOrdersByCustomer ( customerID ) ) {
System.out.println ( order.getID() + " | " + order.getAmount().toString() );
}
如果客户“123
”有订单,则输出将如下所示
12034 | 127.87
15677 | 238.93
到目前为止一切顺利。
但是,如果客户没有订单,会发生什么?
在“避免null
”的世界中,getOrdersByCustomer()
返回一个空列表。因此,我们的代码仍然运行,但没有显示任何内容。
在“喜爱null
”的世界中,getOrdersByCustomer()
返回null
,这会在for
语句中引发NullPointerException
。
这正是“避免null
”方法的倡导者避免null
并返回空列表的原因。他们声称
您的客户端代码更简单,并且适用于所有情况。您不必检查
null
,并且消除了空指针错误的风险。如果您确实需要检查“无数据”的情况,您可以使用isEmpty()
(例如if ( list.isEmpty() ) {
)。简而言之:您的代码更简单,更不容易出错。
然而,现在让我们考虑一下最终用户。我们的迷你应用程序的用户将如何体验“无数据”的情况?
在我们的应用程序的更精细版本中,系统会提示用户输入客户ID。他/她输入“123
”,然后等待结果。
在“喜爱null
”的世界中,应用程序会因空指针错误而崩溃。这不好!
在“避免null
”的世界中,可能会发生以下情况之一,具体取决于结果的显示方式
- 系统控制台中不显示任何内容,或者
- 浏览器显示一个空表(即白屏),或者
- 打印出一张空白纸。
![]() |
客户订单列表
用户会感到惊讶并疑惑:这是什么意思:客户没有订单,还是我的电脑有问题?
毫无疑问,这比应用程序崩溃的挫败感要小。
然而,用户真正想要的是一条清晰明确的消息,例如
No orders yet for customer 123.
![]() |
注意 |
---|---|
我之所以想到这个例子,是因为几周前我(作为最终用户)亲身经历了这种情况。我想查看即将举行的环法自行车赛(每年在法国举行的最受欢迎的自行车比赛)车队中的车手列表。所以我去了一个网站,看到了所有车队的列表。每个车队名称的左侧都有一个按钮,可以放大车队并查看车手列表。我点击了按钮,但什么也没发生。但我注意到我点击的车队名称和下一个车队名称之间的空间稍微增加了。所以我猜测车手尚未确定。服务器软件可能返回了一个空列表,然后该列表被渲染成一个带有填充或边距的空表。这就是为什么我看到页面稍微移动了一下,但没有显示任何数据。我当然更希望看到一条明确的消息,说明车手尚未确定。 |
在这两种情况下,当然都很容易显示适当的消息。
在“避免null
”的世界中,我们按如下方式改进代码
String customerID = "123";
List<ICustomerOrder> result = getOrdersByCustomer ( customerID );
if ( ! result.isEmpty() ) {
// code to display orders ...
} else {
System.out.println ( "No orders yet for customer " + customerID + "." );
}
在“喜爱null
”的世界中,代码变为
String customerID = "123";
List<ICustomerOrder> result = getOrdersByCustomer ( customerID );
if ( result != null ) {
// code to display orders ...
} else {
System.out.println ( "No orders yet for customer " + customerID + "." );
}
正如我们所见,两个版本之间的唯一区别是检查“无订单”
if ( ! result.isEmpty() ) {
与
if ( result != null ) {
这只是一个简单的例子,但已经有一个非常重要的经验教训需要吸取
“无数据”的情况在语义上与“已找到一个或多个元素”的情况不同。
这是一个关键点,尽管没有什么值得惊讶的。在现实生活中,我们一直都很习惯于区分这一点并做出适当的反应,例如
-
“教室空了”的情况与“教室里有一个或多个学生”的情况截然不同。在第一种情况下,老师不需要授课,窗户应该关闭,灯应该关掉,等等。
-
“没有客户投诉”的情况与“一些客户报告软件问题”的情况截然不同。在第一种情况下,我们可以放松,上网,给妈妈打电话,随便什么。在第二种情况下,我们不能。
-
等等。
当然也有例外。有时,我们不需要区分,两种情况可以以相同的方式处理。但这些是例外,很少发生。关键在于,在绝大多数情况下,做出不同的反应很重要。
对于软件开发人员来说,显而易见的结论是:我们应该始终小心谨慎,考虑这两种情况并回答这个问题:我是否需要检查“无数据”,并在“无数据”时做其他事情。
这种区分当然在这两种情况下都可以很好地完成。在“避免null
”的世界中,我们只需检查列表是否为空。在“喜爱null
”的世界中,我们检查null
。在这两种情况下,我们都可以编写完全有效且可靠的代码,让我们的用户满意。所以这不是问题。真正的问题是(在这两种情况下)我们经常忘记做出这种重要的区分——我们忘记检查结果是否是空列表,或者我们忘记检查结果是否是null
。
那么,我们现在必须弄清楚的是:在这两种情况下,忘记检查“无数据”可能带来的后果是什么。
在上面的例子中,我们只是遇到了一个没有灾难性但应该修复的情况。
后果也可能是戏剧性的,我们将看到。
聚合值
想象一家出售房屋的房地产公司。房屋的价格是通过将构成总价的不同组成部分相加来计算的。例如
土地 | 100,000 |
房屋 | 200,000 |
管理费用 | 2,000 |
总价 | 302,000 |
为了对此建模,我们可以定义以下接口
public interface IHouse {
public int getID();
// more attributes not shown here
public Map<String, Double> getPriceComponents();
public Double computeTotalPrice();
}
![]() |
注意 |
---|---|
在实际应用程序中,最好使用Money 类型而不是Double 。 |
假设我们生活在“避免null
”的世界中。那么getPriceComponents()
永远不会返回null
。
computeTotalPrice()
的实现如下
public Double computeTotalPrice() {
Double result = 0.0;
for ( Double component : getPriceComponents().values() ) {
result = result + component;
}
return result;
}
现在假设对于给定房屋,价格组成部分尚未在数据库中定义。我们还不知道价格。
然后getPriceComponents()
返回一个空列表。
并且computeTotalPrice()
返回0
。
这可能是有害的。
为什么?
因为我们假设房屋的售价为0,尽管价格组成部分尚未定义。
这与在类似应用程序中直接输入0作为价格相同,该应用程序只有一个价格字段(没有价格组成部分)。
在现实生活中的商店中,这就像为尚未定价的产品显示0作为价格。应该显示的是根本没有价格,或者显示“请询价”之类的消息。
我们现在遇到的情况与《为什么我们应该喜爱'null'》中“使用零而不是Null”一节所描述的情况相同。可能带来邪恶的后果,例如在公司网站上将房屋价格显示为0,并允许访问者免费获得房屋。
![]() |
售价:0.00
(图片来自martinlabar 许可:CC BY-NC 2.0)
是的,这有点夸张,但夸张的例子有时有助于说明问题。无论如何,想象一家销售数千种低价产品的公司,并故意免费提供其中一些产品(例如:一些Amazon Kindle书籍是免费的)。那么公司中没有人发现错误,客户免费获得非免费产品的可能性就很大。这样的错误在现实生活中确实会发生。
我们当然可以修复这个错误。例如,如果getPriceComponents()
返回一个空列表,我们可以在computeTotalPrice()
中返回null
public Double computeTotalPrice() {
if ( getPriceComponents().isEmpty() ) return null; // bug fixed
// rest of code ...
}
但是这个解决方案引入了不一致性,因为我们现在返回null
,尽管我们生活在“避免null
”的世界中。一些程序员实际上喜欢这个解决方案,因为他们在论坛中告诉我们:“在集合的情况下,我从不返回null
(我返回一个空集合),但在其他情况下,我可能会决定返回null
。”
毫无疑问,这个解决方案可能是可以接受的,因为它允许我们编写正确的代码并消除了显示0作为价格的风险。尽管如此,它只有在我们实际检查“无数据”的情况下才有效。
现在让我们转到“喜爱null
”的世界,这意味着如果组件尚未定义,getPriceComponents()
返回null
。
假设我们像以前一样为computeTotalPrice()
编写相同的代码
public Double computeTotalPrice() {
Double result = 0.0;
for ( Double component : getPriceComponents().values() ) {
result = result + component;
}
return result;
}
现在我们也有一个错误,因为如果getPriceComponents()
返回null
,则会在for
语句中抛出异常。
然而,这个错误与我们在“避免null
”世界中遇到的错误之间有三个显著的区别
-
这个错误更有可能早早被发现。
在开发或测试期间,当代码第一次以
getPriceComponents()
返回null
运行时,会抛出NullPointerException
,程序员/测试人员会立即意识到这个错误。另一方面,产生错误值但不会导致应用程序崩溃的错误在软件发布之前不太可能被发现。
-
此错误的后果不那么严重。
即使该错误逃过了开发人员/测试人员的眼睛并交付给客户,在生产模式下发生的情况是应用程序因
NullPointerException
而崩溃。这根本不好,但显然比默默地继续执行错误数据并冒着严重后果(例如免费赠送非免费产品)的风险痛苦得多。继续执行错误数据是软件可能做的最糟糕的事情之一。想象一下医院里的一位医生,他根据软件提供的错误数据决定如何治疗病人。
-
这种类型的错误更容易调试。
导致
NullPointerException
的错误通常很容易调试,因为原因和结果在空间(即源代码中的位置)和时间上距离很短。例如,在我们的例子中,语句...
for ( Double component : getPriceComponents().values() ) {
...导致了
NullPointerException
。异常在包含错误的 मेथड 中抛出,并且在执行有错误的语句时立即抛出。因此,我们可以立即发现错误并轻松修复它。
另一方面,计算错误结果并默默地继续执行直到稍后才检测到问题的软件更难调试,也更耗时。想象一下最坏的情况,一个应用程序计算错误数据并将其存储在另一个应用程序读取的数据库中。尽管错误在应用程序1中,但故障将出现在应用程序2中!
修复computeTotalPrice()
中的错误是微不足道的。如果getPriceComponents()
是null
,我们只需返回null
public Double computeTotalPrice() {
if ( getPriceComponents() == null ) return null; // bug fixed
Double result = 0.0;
for ( Double component : getPriceComponents().values() ) {
result = result + component;
}
return result;
}
这解决了问题。
现在,如果我们没有关于价格组成部分的数据,那么我们也没有关于总价的数据——我们的数据是一致的。
如果源代码中其他任何地方我们忘记检查computeTotalPrice()
是否返回null
,那么将抛出另一个null
指针错误。
显示0并免费赠送产品的风险不存在。
![]() |
注意 |
---|---|
如果使用 Java 8,我们还可以使用 public Double computeTotalPrice() {
return getPriceComponents().values().stream().mapToDouble(x -> x).sum();
}
然而,这并不会改变上述讨论中的任何内容。行为将完全相同。 |
现在让我们更进一步,假设我们有幸使用提供编译时 null 安全的编程语言。
简而言之,这意味着
-
所有对象引用默认情况下都不可为 null。如果允许
null
,则必须在源代码中明确说明,例如使用nullable
关键字,或在类型名称后加上?
。 -
编译器要求所有可空引用在对其执行操作之前都必须检查
null
。
接口 IHouse
将这样定义(请注意,下面的代码是无效的 Java 代码,因为 Java 不提供 null
安全)
public interface IHouse {
public int getID();
// more attributes not shown here
public nullable Map<String, Double> getPriceComponents();
public nullable Double computeTotalPrice();
}
正如我们所见,nullable
关键字明确表示 getPriceComponents()
和 computeTotalPrice()
可以返回 null
。这是一个绝对重要的信息——不仅对编译器,对 API 的人类读者以及附加工具也一样。我们不必再怀疑这些方法是否可能返回 null
。现在我们知道它们可能返回 null
,我们必须考虑这一点。
但真正令人兴奋的是,如果我们忘记检查null
,编译器现在会抱怨。以下代码会生成编译时错误,因为getPriceComponents()
可能为null
,而我们没有检查
public Double computeTotalPrice() {
Double result = 0.0;
for ( Double component : getPriceComponents().values() ) {
result = result + component;
}
return result;
}
空安全帮助我们更快地编写更可靠的代码,因为如果我们忘记检查null
(这在实践中经常发生),编译器会提醒我们。我们必须像这样修复代码
public Double computeTotalPrice() {
if ( getPriceComponents() == null ) return null;
Double result = 0.0;
for ( Double component : getPriceComponents().values() ) {
result = result + component;
}
return result;
}
![]() |
注意 |
---|---|
有关空安全以及支持空安全的语言的概述,请参阅上一篇文章《为什么我们应该喜爱'null'》的“那么,有没有更好的解决方案?”一节。 |
让我们回到“避免null
”的世界,再次看看computeTotalPrice()
的实现,它使用了流(Java 8 中引入)
public Double computeTotalPrice() {
return getPriceComponents().values().stream().mapToDouble(x -> x).sum();
}
我们之前看到,如果computeTotalPrice()
在空列表的情况下返回null
,则更不容易出错。一个有趣的问题出现了:那么,如果java.util.stream.IntStream
中的sum()
方法(我们在上面的代码中使用的)在空列表的情况下返回null
而不是0,会不会更好呢?
不!这不行,因为空列表也可能意味着“确实有一个列表,我们知道它是空的”,这与说“我们没有关于列表的数据”不同。在第一种情况下,总和确实是零,但在第二种情况下我们不知道总和。然而,在“避免空”的世界中,这两种情况都将使用空列表。因此,如果我们对空列表使用sum()
,我们必须考虑空列表的含义,然后决定值零是否可以接受。我们必须小心。
好的,这是另一个问题
java.util.stream.IntStream
中的min()
方法可用于获取整数列表中最小值。如果流是由空列表提供的,该方法应该返回什么?它也应该返回0(像sum()
一样),还是应该返回32位有符号整数的最低负值,还是应该返回null
。
这次,除了返回null
别无选择,因为即使空列表意味着“确实有一个列表,我们知道它是空的”,计算最小值也没有意义。如果我们查看min()
的 API 文档,我们可以看到这确实是返回的结果。但是,min()
不返回null
,而是返回OptionalInt
,因为这是 Java 8 中用于可能返回“无数据”的方法的模式。(注意:有关Optional
模式(而不是返回null
)的讨论,请参阅上一篇文章《为什么我们应该喜爱'null'》的“Optional/Maybe 模式”一节。)
正如我们所见,需要相当多的谨慎。
与此相比,在“喜爱null
”的世界中,我们必须考虑什么。如果一个列表是null
,那么它的总和、最小值、最大值、平均值以及所有其他值也都只是null
。简单!
一个有缺陷的投票应用程序
这里是最后一个,稍微复杂一些的例子,说明如果我们使用空列表来表示“无数据”可能会发生什么。
想象一个投票应用程序,用于在3位候选人中选出获胜者。每位候选人的分数在五个地区收集。获胜者是五个地区平均得分最高的候选人。
假设应用程序中使用以下函数来汇总给定地区给定候选人的选民个人分数
public static Integer computeTotalScore ( List<Integer> scoresOfVoters ) {
Integer totalScore = 0;
for ( Integer score : scoresOfVoters ) {
totalScore = totalScore + score;
}
return totalScore;
}
![]() |
注意 |
---|---|
如前所述,我们也可以使用流(Java 8 版本中可用)来计算总数 public static Integer computeTotalScore ( List<Integer> scoresOfVoters ) {
return scoresOfVoters.stream().mapToInt(x -> x).sum();
}
然而,无论我们是否使用流,都不会改变下面的讨论。 |
现在假设所有分数都已收集并输入到数据库中,除了候选人2在区域3中的分数。
如果我们生活在“避免null
”的世界中,那么computeTotalScore()
将以空列表作为输入被调用并返回0。
下表显示了堆中某个位置保存的数据示例
候选人1 | 候选人2 | 候选人3 | |
---|---|---|---|
Average | 370.4 | 422.8 | 478.8 |
区域1 | 251 | 154 | 485 |
区域2 | 548 | 652 | 145 |
区域3 | 412 | 0 | 965 |
区域4 | 496 | 954 | 415 |
区域5 | 145 | 354 | 384 |
请注意候选人2在区域3中的值为0。
后果是严重的!我们的投票应用程序可能会选出错误的候选人作为获胜者。它报告候选人3为获胜者,尽管实际上候选人2可能才是获胜者。如果候选人2在区域3的实际分数低于280,那么我们很幸运——候选人3确实是获胜者。如果缺失的分数为280,那么候选人2和3之间打平。如果缺失的分数大于280,那么真正的获胜者将是候选人2。(注意:我使用电子表格计算了这些值。)
![]() |
注意 |
---|---|
我们还可以想象,表中的分数在-100到+100之间。那么0将是表中一个完全有效的值,发现错误的机会将大大减少。 |
![]() |
获胜者是……3号
(图片来自foilman 许可:CC BY-SA 2.0))
如果我们喜爱null
,这种情况就不会发生。在“候选人2在区域3中尚无数据”的情况下,computeTotalScore()
会立即抛出NullPointerException
。如果这发生在生产模式下,确实令人尴尬,但选出错误候选人的不可接受的风险并不存在。
空间和时间考虑
这很简单。
就空间和时间而言,null
总是便宜的。
另一方面,创建空列表需要时间,存储它们需要空间——在内存中、本地磁盘上、远程计算机上或任何地方。并且在它们不再使用后,必须回收内存(手动或由垃圾收集器自动回收)。
为了缓解这个问题,我们应该始终使用不可变和共享的空列表(如果我们无法返回null
,无论出于何种原因)。在 Java 中,我们应该使用Collections.emptySet()
、Collections.emptyList()
或Collections.emptyMap()
方法,这些方法返回用于通用集合的不可变共享对象。以下是它们的使用示例
public static List<String> getStringList() {
List<String> result;
// if ( there_are_data ) {
result = new ArrayList<String>();
result.add ( "foo" );
result = Collections.unmodifiableList ( result );
// } else {
result = Collections.<String>emptyList();
// }
return result;
}
如果我们使用null
,检查“无数据”也更有效率。像这样的测试
if ( list == null )
执行速度快于
if ( list.isEmpty() )
![]() |
注意 |
---|---|
为了有一个概念,我做了一个快速而粗糙的测试,在我的环境中,结果是if ( list == null ) 大约快了3倍。性能差异当然可能很大,这取决于许多因素,例如编译器、JVM、硬件架构等。 |
无论如何,结论是显而易见的
说到时间和空间,null
总是赢家。
API考虑
在这两种情况下,另一个有趣的方面是函数返回的集合之间的 API 差异。
在下面的讨论中,我们假设
-
在“避免
null
”的世界中-
我们返回一个包含0个或更多元素的不可变列表。
-
如果没有数据,我们返回一个空列表。
-
-
在“喜爱
null
”的世界中-
我们返回一个包含1个或更多元素的不可变列表。
-
如果没有数据,我们返回
null
。
-
让我们考虑以下简化接口
public interface IImmutableList<E> {
// returns true if the list is empty
boolean isEmpty();
// returns the number of elements in the list
int size();
// get the first element in the list
E first();
// get the last element in the list
E last();
// get an iterator to loop over all elements
Iterator<E> iterator();
// more features
// ...
}
下表显示了允许为空的列表和始终至少包含一个元素的列表之间的 API 差异。
方法 | 可空列表 (0个或更多元素) |
不可空列表 (1个或更多元素) |
---|---|---|
isEmpty() |
有用 | 没有意义 应该删除 |
size() |
返回0或正整数值 | 返回正整数值 (永不返回0) |
first() last() |
如果列表为空,则抛出异常 | 从不抛出异常 |
iterator() |
可以,但如果列表为空则没有意义 | 可以 |
我们可以清楚地看到,不可空列表具有更简单的 API,并且更不容易出错
-
isEmpty()
可以废弃。我们永远不必写这样的东西if ( customersFound != null && ! customersFound.isEmpty() ) { // ... }
-
size()
从不返回 0。这是一个优势,因为我们都知道,0 在数学中是一个非常特殊的值,具有独特的性质。如果它出现在我们的应用程序中,我们必须小心,因为它可能导致微妙的错误——最臭名昭著的是除以零。例如,如果输入是空列表,以下代码中会发生“除以零”错误,因为
list.size()
返回0public static int computeAverage ( List<Integer> list ) { int sum = list.stream().mapToInt(x -> x).sum(); return sum / list.size(); }
注意 最好将 size()
的返回类型声明为“不包括零的无符号正整数”,因为这将增加编译时类型安全性。有些语言提供这种类型,但 Java 不提供。 -
first()
和last()
没有抛出异常的风险,因为列表永远不会为空。
简而言之
表示不含任何元素(即空列表)的需求增加了复杂性,并使列表更容易出错。
此外,值得注意的是,许多(如果不是大多数)操作在空列表的情况下根本没有意义。有些操作在执行时没有害处,有些则不返回任何有意义的值,还有一些可能会抛出异常。例如,在空列表中搜索元素没有意义,删除元素或对其排序也没有意义。有人可能会争辩说,遍历空列表或从空列表中“删除所有元素”没有害处。但是,正如我们已经看到的,最好在代码中检查“无数据”,并思考在当前上下文中“无数据”意味着什么,然后做出适当的反应。此外,执行计算聚合值(总和、平均值、最小值、最大值等)并为空列表返回非 null 值的操作容易出错,正如我们之前在一节中已经看到的。
如果我们返回不可变、非空列表或null
来表示“无数据”,所有这些问题都将消失。
这是否意味着在“喜爱null
”的世界中,我们永远不应该返回可变或空集合?
不!
众所周知,我们应该始终优先选择不可变对象(包括不可变集合),而不是可变对象。不可变对象没有状态转换,可以在并行计算环境中自由共享,并且更不容易出错。
然而,有时(很少)函数的作用是返回一个可变集合,该集合可以在函数返回后完成或更新。
此外,有时(很少)在“喜爱null
”的世界中返回一个空集合(或null
)是有意义的。
考虑一个返回一盒饼干的函数。如果没有盒子,函数返回null
。如果有一个盒子,但是空的,函数返回一个空集合。否则,它返回一个非空集合。如果函数返回后饼干可以被吃掉,则集合应该是可变的,否则应该是不可变的。
简而言之:在“喜爱null
”的世界中,函数返回的集合类型通常是
- 大多数情况下是不可变且不可为空的
- 有时(很少)是可变的
- 有时(很少)是可空的
总结与结论
以下是需要记住的最重要几点总结
返回null
而不是空集合可能会带来更好的性能,因为空列表的创建和丢弃需要时间,存储它们需要空间。为了最大限度地减少这种不便,我们应该返回共享的不可变空集合。
如果使用得当,“返回空列表”方法和“返回null
”方法都可以让我们编写正确可靠的代码。
当我们忘记考虑“无数据”和“1个或更多元素”的不同语义时(即我们忘记检查空列表或忘记检查null
),这两种方法都会出现问题。
忘记区分可能造成的后果在这两种方法中是不同的
-
如果我们采用“返回空集合”的方法
-
结果是不可预测的,从完全无害到极其有害。
-
在许多情况下,计算出的值是错误的,程序执行会继续,而不会出现错误消息。默默地继续使用错误的值可能会导致灾难。
-
-
如果我们采用“返回
null
”的方法-
大多数情况下,结果是可预测的:会发生空指针错误,应用程序崩溃(除非空指针错误被捕获并妥善处理)。
-
两种结果都是不可取的。但是,由于空指针错误导致的应用程序崩溃(大多数情况下)比程序默默地继续并提供错误结果或导致其他不可接受的结果或灾难要不那么可怕。“返回null
”方法显然支持重要的快速失败!原则。这在开发模式下尤为重要且完全无害,有助于在更短的时间内编写更可靠的代码。
简而言之:“返回null
”方法
- 导致程序执行更快(可能不明显)
- 占用更少内存(可能微不足道)
- 可以使用更简单、更不易出错的集合
- 最重要的是:降低因 bug 导致可怕后果的风险
最终结论是显而易见的
如果我们有选择,我们应该使用“返回null
”方法。
![]() |
注意 |
---|---|
有时,我们没有选择。例如,如果我们在一个“返回空集合而不是null ”是规则并且团队所有成员都应用此规则的环境中工作,那么我们也应该这样做,即使我们确信返回null 更好。逆流而动会造成不一致并导致其他邪恶问题。 |
如果我们在“返回null
”的同时使用内置编译时空安全性的编程语言,则完全消除了忘记检查“无数据”的风险。在这种环境中,本文中描述的所有错误都会在编译时被检测到,并且不会在运行时发生。这是理想的解决方案。如果有选择,我们应该始终选择它。
致读者:欢迎评论,特别是如果本文遗漏了其他需要考虑的要点,例如这两种方法的其他优点/缺点。
在本系列文章的下一部分中,我们将探讨现实生活中的空列表和null
。它们是否存在?如何处理它们?您可以期待一些轻松而富有启发性的娱乐。
相关文章链接
- 返回空列表而不是null真的更好吗?
- 为什么我们应该拥抱“null”