65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (33投票s)

2014年9月17日

LGPL3

9分钟阅读

viewsIcon

86642

downloadIcon

694

第三部分:空列表在现实生活中的应用

目录

Home

第三部分:现实生活中的空列表

引言

上一期中,我们看到了为什么当函数调用使用“无数据”返回时,最好返回 null 而不是空列表。例如,以下 Java 函数...

public static List<ICustomerOrder> getOrdersByCustomer ( String customerID )

... 如果给定客户没有订单,则应返回 null(而不是空列表)。正如我们所见,这可以降低因错误导致的糟糕结果的风险,并具有其他优势。

在本期系列文章中,我们将探讨我们在现实生活中如何使用列表。

在思考如何设计或实现软件时,我发现考虑事物在现实生活中是如何运作的,通常很有用且富有启发性。

那么,关于现实生活中的列表,我们可以说些什么?

它们存在吗?

它们经常出现吗?

现实生活中的例子证实了我们从上一期得出的结论吗?

让我们来看看。

爱丽丝的购物清单

想象一下爱丽丝和鲍勃的以下故事

爱丽丝:“鲍勃,请你去杂货店买些食物好吗?”

鲍勃:“当然!”

爱丽丝给了鲍勃一张购物清单。

鲍勃开车去了杂货店。他看了看清单,发现它是……空的。

Alice's shopping list

爱丽丝的购物清单

鲍勃开车回家,给了爱丽丝一个空盒子。

鲍勃:“这是你要我买的食物。”

爱丽丝:“谢谢亲爱的。”

我们在现实生活中认为荒谬、愚蠢或有趣的事情,在软件执行的世界里已经(并将继续)发生过数十亿次。每次(在避免 null 的环境中)一个函数返回空列表(而不是 null)来表示“无数据”时,都会发生这种情况。

有人可能会争辩说,在软件中处理空列表不会消耗大量的时间和资源——这与鲍勃花了时间和使用了他的汽车形成对比。确实,在许多情况下,处理空列表所需的时间和资源微不足道。然而,仍然存在时间和空间上的损失,而且后果可能非常可怕。试想一下,在最坏的情况下,需要建立网络连接,并且需要在计算机之间交换数据来处理空列表。然后,偶尔可能会出现网络连接失败,导致系统完全崩溃,因为“什么都不做”的操作无法执行。这就像鲍勃开车去杂货店“什么都不买”时,汽车引擎发生故障(或发生更糟的事情)。

另一方面,正如我们在上一期中看到的,null 在时间和空间上总是廉价的。

鲍勃的邮票

这是另一个现实生活中的例子

鲍勃收集邮票。

爱丽丝不收集邮票。

鲍勃有一个标有“邮票”的盒子,里面装着他的邮票。

这是否意味着爱丽丝有一个空的邮票盒子,因为她收集邮票?

不,当然不是!她根本没有邮票盒子。

试想一下,世界上每个人都为他们不收集的每样东西都有一个空盒子。太奇怪了!

那么,如果一个人不收集邮票,下面的 Java 接口中的 getPostageStamps 应该返回什么?空列表还是 null

interface IPerson {

   public String getName();
   // ... more attributes

   public List<IPostageStamp> getPostageStamps();
}

返回空列表就像爱丽丝有一个空盒子。

返回 null 就像爱丽丝根本没有盒子。

[Note] 注意

在支持 Optional/Maybe 模式的语言中,另一种解决方案是始终返回一个非 null 的 OptionalMaybe 对象。Optional/Maybe 模式的基本思想是:

与其提供一个非 null 值或 null,不如始终提供一个非 null 的容器对象,该对象要么包含一个值,要么不包含一个值。

有关此解决方案的讨论,请参阅我之前的文章为什么我们应该喜欢 ‘null’(“Optional/Maybe 模式”一章)。

这种模式在现实生活中流行吗?我认为不是。想象一下我们例子中的这种模式的应用。爱丽丝不收集邮票。因此,她有一个包含……什么的盒子。鲍勃收集邮票。所以,他有一个包含一个盒子,而这个盒子又包含邮票。

卢森堡的客户

假设我们有以下方法按城市获取客户列表

public static List<ICustomer> getCustomersByCity ( String city ) {
   // code to retrieve customers from database and return the result
}

如果数据库中没有给定城市的客户,此方法应返回什么?

如果我们阅读了许多讨论这类问题的论坛,我们可以看到通常有以下三种不同的答案,按受欢迎程度排序(第一个是最受欢迎的):

  1. 该方法应返回空列表
  2. 该方法应返回 null
  3. 该方法应抛出异常

另一个重要的问题是:在运行时遇到资源错误(如数据库连接错误)时,此方法应如何行为?同样,这取决于你问谁,你会得到不同的答案。

为了找到正确的答案,让我们考虑一下在现实生活中将如何处理这种情况。

想象一下

大老板对助手说:“我需要一份我在卢森堡所有客户的名单。你能帮我做一下吗?”

助手照办了。他的任务是在公司的 ERP 软件中运行查询,以打印一份卢森堡客户的名单。

有三种可能的结果

  1. 公司在卢森堡有客户

    助手打印出名单并交给老板。
  2. 公司在卢森堡没有客户

    助手告诉老板:“我们在卢森堡没有客户。”

    重要提示:在正常情况下,助手不会给老板一张空白的纸来表明没有客户,对吧?

  3. 助手因技术问题无法执行查询(例如:他忘了密码(因为他早上改了密码(但忘了写在便利贴上,而便利贴放在一个秘密的地方(即贴在他的台式显示器正面))))

    助手告诉老板:“抱歉,我无法打印名单,因为……”

现在,我们软件设计问题的正确答案几乎很明显了,不是吗?

  1. 第一种情况无需讨论。如果卢森堡有客户,则 getCustomersByCity 返回一个非空列表。

  2. 如果我们模仿现实世界,那么在情况 2(卢森堡没有客户)下,我们显然会返回 null。返回空列表就像助手给老板一张空白的纸。没有人会在现实生活中这样做——这没有意义,至少在“正常”情况下是这样。

    除非有非常充分的理由,否则我们在软件中也不需要这样做。

    例如,想象一下老板真的需要一张纸,即使没有客户。他/她想要一份像这份……

    ... 这样的“证明”以便归档。

    如果我们想模拟这种情况,我们可能会倾向于实际返回一个空列表。但更好的解决方案是返回一个非 null 对象,该对象实现以下 Java 接口:

    interface ICustomerByCityReport {
    
       public String getCity();
    
       public Date getDateOfReport();
    
       public List<ICustomer> getCustomers();
    }

    在给定城市没有客户的情况下,该方法将返回一个非 null 的 ICustomerByCityReport 对象,其中 getCustomers() 返回 null

  3. 情况 3(即由于技术问题操作无法执行)不太明显。我们应该只返回 null 吗?不!因为这就像助手告诉老板:“卢森堡没有客户。”显然,“卢森堡没有客户”的信息与“由于技术问题我无法打印名单。我无法告诉您是否有卢森堡的客户”在语义上非常不同。这对老板来说是一个重要的区别,只有他/她才能决定该怎么做。对于调用 getCustomersByCity 的客户端代码也是如此。必须将“发生技术问题”的信息传递给客户端,由客户端决定该怎么做。

    如何做到这一点取决于我们使用的编程语言。

    在支持异常机制的语言(C#、Java 等)中,如果无法从数据库检索数据(例如,无法建立数据库连接),我们应抛出异常。

    在其他语言(例如,支持多个输出参数或元组的语言)中,我们可能会返回两个值——第一个是 result,第二个是 error。然后可能出现以下返回状态:

    • 操作成功并找到结果:result 包含一个(非空)列表,而 errornull

    • 操作成功但未找到结果:resulterror 均为 null

    • 操作无法执行:resultnull,而 error 包含描述错误的 * 象

现实生活中的空列表

如果我们环顾四周,看看物理世界,我们可以很快发现空列表在现实生活中非常罕见。大多数时候,我们要么有一个非空列表,要么根本没有列表。例如,你可能有一个待办事项清单。如果你有一个,它很可能(像我的)不是空的。

然而,显而易见但同样有趣的是,每次我们开始创建一个非空列表时,都会出现空列表。

想象一下爱丽丝正在为鲍勃写购物清单

  1. 她拿出一张纸,这张纸是空的——一个空列表。
  2. 她写下了所有要买的物品。最后,列表就不是空的了。此外,在编写过程中,列表是可变的——物品被添加(有些可能会被移除或修改)。
  3. 一旦列表创建完成,它就变得不可变的——列表不再改变。

这就是我们在现实生活中通常的做法。这恰恰反映了我们应该如何在软件中进行。这是一个简单的 Java 示例:

public static List<String> getShoppingList() {

   // 1. create a mutable empty list
   List<String> result = new ArrayList<String>();

   // 2. populate the list      
   result.add ( "Almonds" ); 
   result.add ( "Coconut oil" );
   result.add ( "Avocado" );
   result.add ( "Blueberries" );

   // 3. return an immutable, non-empty list
   return Collections.unmodifiableList ( result );
}

结论

不可变的空列表在现实生活中非常罕见。通常,我们要么有一个非空列表,要么根本没有列表。

推论是,除非有特殊情况,否则我们不应该在软件应用程序中使用不可变的空列表。这与我们从上一期得出的结论相符,并证实了通常最好返回 null 而不是空列表。

下一期中,我们将探讨真实的源代码示例。我们将研究实践中经常发生的典型情况。我们将看到如何处理异常情况,例如无效输入参数值和资源错误。

相关文章链接

© . All rights reserved.