返回 null 或空集合更好吗?
Posted
技术标签:
【中文标题】返回 null 或空集合更好吗?【英文标题】:Is it better to return null or empty collection? 【发布时间】:2010-12-30 12:16:12 【问题描述】:这是一个普遍的问题(但我使用的是 C#),最好的方法是什么(最佳实践),对于将集合作为返回类型的方法,您是返回 null 还是空集合?
【问题讨论】:
嗯,不完全是 CPerkins。 rads.***.com/amzn/click/0321545613 @CPerkins - 是的,有。 Microsoft 自己的 .NET 框架设计指南中明确说明了这一点。有关详细信息,请参阅 RichardOD 的答案。 仅当含义是“我无法计算结果”时才返回 null。 Null 永远不应该有“空”的语义,只有“缺失”或“未知”。更多细节在我关于这个主题的文章中:blogs.msdn.com/ericlippert/archive/2009/05/14/… Bozho:“任何”是非常多的语言。 Common Lisp 中的空列表完全等于空值呢? :-) 实际上,“重复”是关于返回对象而不是集合的方法。这是一个不同的场景,有不同的答案。 【参考方案1】:来自Framework Design Guidelines 2nd Edition(第 256 页):
不要从 集合属性或方法 返回收藏。返回一个空 集合或空数组。
这是另一篇关于不返回 null 的好处的有趣文章(我试图在 Brad Abram 的博客上找到一些东西,他链接到了这篇文章)。
编辑-正如 Eric Lippert 现在对原始问题的评论,我也想link to his excellent article。
【讨论】:
+1。遵循框架设计指南始终是最佳实践,除非您有充分的理由不这样做。 @Will,绝对的。这就是我所遵循的。我从来没有发现任何需要这样做 是的,这就是你所遵循的。但如果你依赖的 API 不遵循它,你就会被“困住”;) @Bozho-yeap。您的回答为这些边缘案例提供了一些不错的答案。【参考方案2】:从管理复杂性这一主要软件工程目标的角度来看,我们希望避免向 API 的客户端传播不必要的圈复杂性。向客户端返回 null 就像返回另一个代码分支的圈复杂度成本。
(这对应于单元测试负担。除了空集合返回案例之外,您还需要为 null 返回案例编写测试。)
【讨论】:
【参考方案3】:我称之为我的十亿美元错误……当时,我正在设计第一个综合类型系统,用于面向对象语言中的引用。我的目标是确保所有引用的使用都应该是绝对安全的,并由编译器自动执行检查。但我无法抗拒放入空引用的诱惑,仅仅是因为它很容易实现。这导致了无数的错误、漏洞和系统崩溃,在过去的四十年中可能造成了十亿美元的痛苦和损失。 – ALGOL W. 的发明者 Tony Hoare。
请参阅here,了解关于null
的精心设计的垃圾风暴。我不同意undefined
是另一个null
的说法,但仍然值得一读。它解释了为什么你应该完全避免null
而不仅仅是在你问的情况下。本质是,null
在任何语言中都是一个特例。您必须将null
视为一个例外。 undefined
在这方面有所不同,处理未定义行为的代码在大多数情况下只是一个错误。 C 和大多数其他语言也有未定义的行为,但其中大多数在语言中没有标识符。
【讨论】:
【参考方案4】:空集合。总是。
这很糟糕:
if(myInstance.CollectionProperty != null)
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
在返回集合或可枚举时,永远不要返回 null
被认为是最佳实践。 总是返回一个空的枚举/集合。它可以防止上述废话,并防止您的汽车被您的同事和班级的用户怂恿。
在谈论属性时,总是设置你的属性一次然后忘记它
public List<Foo> Foos public get; private set;
public Bar() Foos = new List<Foo>();
在 .NET 4.6.1 中,您可以将其压缩很多:
public List<Foo> Foos get; = new List<Foo>();
在谈论返回可枚举的方法时,您可以轻松地返回一个空的可枚举而不是 null
...
public IEnumerable<Foo> GetMyFoos()
return InnerGetFoos() ?? Enumerable.Empty<Foo>();
使用Enumerable.Empty<T>()
可以被视为比返回(例如,新的空集合或数组)更有效。
【讨论】:
我同意威尔的观点,但我认为“总是”有点过分。虽然空集合可能意味着“0 个项目”,但返回 Null 可能意味着“根本没有集合” - 例如。如果你正在解析 html,寻找一个 id="foo" 的 , 可能会返回空集合;如果没有 id="foo" 的 ,则返回 null 会更好(除非您想处理这种情况并出现异常) 这并不总是“您是否可以轻松返回空数组”的问题,而是空数组在当前上下文中是否可能具有误导性的问题。一个空数组实际上意味着什么,null 也是如此。说你应该总是返回一个空数组而不是 null,几乎就像你说布尔方法应该总是返回 true 一样被误导。两个可能的值都传达了一个含义。 您实际上应该更喜欢返回 System.Linq.Enumerable.EmptyIEnumerable
还是ICollection
并不重要。无论如何,如果你选择 ICollection
类型的东西,它们也会返回 null
... 我希望它们返回一个空集合,但我遇到它们返回 null
,所以我想我会在这里提一下.我会说可枚举集合的默认值为空而不是空。我不知道这是一个如此敏感的话题。
相关(CodeProject 文章): Is it Really Better to 'Return an Empty List Instead of null'?【参考方案5】:
在大多数情况下,返回一个空集合会更好。
原因是调用者实现方便、合约一致、实现更容易。
如果方法返回 null 表示结果为空,则调用者必须实现除枚举之外的 null 检查适配器。 然后这段代码在不同的调用者中重复,所以为什么不把这个适配器放在方法中以便它可以被重用。
对 IEnumerable 有效地使用 null 可能表示结果不存在或操作失败,但在这种情况下,应考虑其他技术,例如引发异常。
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
namespace ***.EmptyCollectionUsageTests.Tests
/// <summary>
/// Demonstrates different approaches for empty collection results.
/// </summary>
class Container
/// <summary>
/// Elements list.
/// Not initialized to an empty collection here for the purpose of demonstration of usage along with <see cref="Populate"/> method.
/// </summary>
private List<Element> elements;
/// <summary>
/// Gets elements if any
/// </summary>
/// <returns>Returns elements or empty collection.</returns>
public IEnumerable<Element> GetElements()
return elements ?? Enumerable.Empty<Element>();
/// <summary>
/// Initializes the container with some results, if any.
/// </summary>
public void Populate()
elements = new List<Element>();
/// <summary>
/// Gets elements. Throws <see cref="InvalidOperationException"/> if not populated.
/// </summary>
/// <returns>Returns <see cref="IEnumerableT"/> of <see cref="Element"/>.</returns>
public IEnumerable<Element> GetElementsStrict()
if (elements == null)
throw new InvalidOperationException("You must call Populate before calling this method.");
return elements;
/// <summary>
/// Gets elements, empty collection or nothing.
/// </summary>
/// <returns>Returns <see cref="IEnumerableT"/> of <see cref="Element"/>, with zero or more elements, or null in some cases.</returns>
public IEnumerable<Element> GetElementsInconvenientCareless()
return elements;
/// <summary>
/// Gets elements or nothing.
/// </summary>
/// <returns>Returns <see cref="IEnumerableT"/> of <see cref="Element"/>, with elements, or null in case of empty collection.</returns>
/// <remarks>We are lucky that elements is a List, otherwise enumeration would be needed.</remarks>
public IEnumerable<Element> GetElementsInconvenientCarefull()
if (elements == null || elements.Count == 0)
return null;
return elements;
class Element
/// <summary>
/// http://***.com/questions/1969993/is-it-better-to-return-null-or-empty-collection/
/// </summary>
class EmptyCollectionTests
private Container container;
[SetUp]
public void SetUp()
container = new Container();
/// <summary>
/// Forgiving contract - caller does not have to implement null check in addition to enumeration.
/// </summary>
[Test]
public void UseGetElements()
Assert.AreEqual(0, container.GetElements().Count());
/// <summary>
/// Forget to <see cref="Container.Populate"/> and use strict method.
/// </summary>
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void WrongUseOfStrictContract()
container.GetElementsStrict().Count();
/// <summary>
/// Call <see cref="Container.Populate"/> and use strict method.
/// </summary>
[Test]
public void CorrectUsaOfStrictContract()
container.Populate();
Assert.AreEqual(0, container.GetElementsStrict().Count());
/// <summary>
/// Inconvenient contract - needs a local variable.
/// </summary>
[Test]
public void CarefulUseOfCarelessMethod()
var elements = container.GetElementsInconvenientCareless();
Assert.AreEqual(0, elements == null ? 0 : elements.Count());
/// <summary>
/// Inconvenient contract - duplicate call in order to use in context of an single expression.
/// </summary>
[Test]
public void LameCarefulUseOfCarelessMethod()
Assert.AreEqual(0, container.GetElementsInconvenientCareless() == null ? 0 : container.GetElementsInconvenientCareless().Count());
[Test]
public void LuckyCarelessUseOfCarelessMethod()
// INIT
var praySomeoneCalledPopulateBefore = (Action)(()=>container.Populate());
praySomeoneCalledPopulateBefore();
// ACT //ASSERT
Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
/// <summary>
/// Excercise <see cref="ArgumentNullException"/> because of null passed to <see cref="Enumerable.CountTSource(System.Collections.Generic.IEnumerableTSource)"/>
/// </summary>
[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void UnfortunateCarelessUseOfCarelessMethod()
Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
/// <summary>
/// Demonstrates the client code flow relying on returning null for empty collection.
/// Exception is due to <see cref="Enumerable.FirstTSource(System.Collections.Generic.IEnumerableTSource)"/> on an empty collection.
/// </summary>
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void UnfortunateEducatedUseOfCarelessMethod()
container.Populate();
var elements = container.GetElementsInconvenientCareless();
if (elements == null)
Assert.Inconclusive();
Assert.IsNotNull(elements.First());
/// <summary>
/// Demonstrates the client code is bloated a bit, to compensate for implementation 'cleverness'.
/// We can throw away the nullness result, because we don't know if the operation succeeded or not anyway.
/// We are unfortunate to create a new instance of an empty collection.
/// We might have already had one inside the implementation,
/// but it have been discarded then in an effort to return null for empty collection.
/// </summary>
[Test]
public void EducatedUseOfCarefullMethod()
Assert.AreEqual(0, (container.GetElementsInconvenientCarefull() ?? Enumerable.Empty<Element>()).Count());
【讨论】:
【参考方案6】:在我看来,您应该返回在上下文中语义正确的值,无论它是什么。一条“总是返回一个空集合”的规则对我来说似乎有点简单。
假设在一个医院系统中,我们有一个函数应该返回过去 5 年所有以前住院的列表。如果客户没有住院,返回一个空列表是很有意义的。但是,如果客户将准入表格的那部分留空怎么办?我们需要一个不同的值来区分“空列表”与“没有答案”或“不知道”。我们可以抛出异常,但这不一定是错误条件,也不一定会让我们脱离正常的程序流程。
我经常对无法区分零和无答案的系统感到沮丧。我有很多次系统要求我输入一些数字,我输入零,然后我收到一条错误消息,告诉我必须在此字段中输入一个值。我刚刚做了:我输入了零!但它不会接受零,因为它无法区分它和没有答案。
回复桑德斯:
是的,我假设“此人未回答问题”和“答案为零”之间存在差异。这就是我回答的最后一段的重点。许多程序无法将“不知道”与空白或零区分开来,这在我看来是一个潜在的严重缺陷。例如,大约一年前我正在买房子。我去了一个房地产网站,上面列出了许多要价为 0 美元的房子。对我来说听起来不错:他们免费赠送这些房子!但我确信可悲的现实是他们只是没有输入价格。在这种情况下,你可能会说,“嗯,显然为零意味着他们没有输入价格——没有人会免费赠送房子。”但该网站还列出了各个城镇房屋的平均要价和售价。我不禁想知道平均值是否不包括零,从而在某些地方给出了错误的低平均值。即 100,000 美元的平均值是多少; 120,000 美元;和“不知道”?从技术上讲,答案是“不知道”。我们可能真正想看到的是 110,000 美元。但我们可能会得到 73,333 美元,这是完全错误的。另外,如果我们在用户可以在线订购的网站上遇到这个问题怎么办? (不太可能适用于房地产,但我相信您已经看到许多其他产品都这样做了。)我们真的希望将“尚未指定的价格”解释为“免费”吗?
RE 有两个独立的功能,一个“有吗?”和“如果是,那是什么?”是的,你当然可以这样做,但你为什么要这样做?现在调用程序必须进行两次调用而不是一次。如果程序员没有调用“any”会发生什么?并直奔“这是什么?” ?该程序会返回一个误导性的零吗?抛出异常?返回一个未定义的值?它会创建更多代码、更多工作和更多潜在错误。
我看到的唯一好处是它使您能够遵守任意规则。这条规则有什么好处,值得费心去遵守它吗?如果不是,那何必呢?
回复 Jammycakes:
考虑实际代码的样子。我知道问题说的是 C#,但如果我写 Java,请原谅。我的C#不是很犀利,原理是一样的。
返回空值:
HospList list=patient.getHospitalizationList(patientId);
if (list==null)
// ... handle missing list ...
else
for (HospEntry entry : list)
// ... do whatever ...
使用单独的功能:
if (patient.hasHospitalizationList(patientId))
// ... handle missing list ...
else
HospList=patient.getHospitalizationList(patientId))
for (HospEntry entry : list)
// ... do whatever ...
它实际上是一个或两行代码,返回 null,所以它不是给调用者更多的负担,而是更少。
我看不出它是如何造成 DRY 问题的。这不像我们必须执行两次调用。如果我们总是想在列表不存在时做同样的事情,也许我们可以将处理下推到 get-list 函数而不是让调用者来做,因此将代码放在调用者中将违反 DRY。但我们几乎可以肯定不想总是做同样的事情。在我们必须处理列表的函数中,缺少列表是一个很可能会停止处理的错误。但是在编辑屏幕上,如果他们还没有输入数据,我们肯定不想停止处理:我们想让他们输入数据。因此,必须以一种或另一种方式在调用者级别处理“无列表”。我们是否使用 null 返回或单独的函数来执行此操作对更大的原则没有影响。
当然,如果调用者不检查空值,程序可能会因空指针异常而失败。但是,如果有一个单独的“获取任何”函数并且调用者没有调用该函数而是盲目地调用“获取列表”函数,那么会发生什么?如果它抛出异常或以其他方式失败,那么这与如果它返回 null 并且没有检查它所发生的情况几乎相同。如果它返回一个空列表,那就错了。您无法区分“我有一个零元素的列表”和“我没有一个列表”。这就像当用户没有输入任何价格时,价格返回零:这是错误的。
我看不出将附加属性附加到集合有什么帮助。调用者仍然需要检查它。这比检查 null 有什么好处?同样,可能发生的最糟糕的事情是程序员忘记检查它,并给出错误的结果。
如果程序员熟悉 null 意思是“没有值”的概念,返回 null 的函数并不奇怪,我认为任何有能力的程序员都应该听说过,无论他是否认为这是一个好主意或不。我认为拥有一个单独的功能更像是一个“惊喜”问题。如果程序员不熟悉 API,当他在没有数据的情况下运行测试时,他会很快发现有时他会返回 null。但是他怎么会发现另一个函数的存在,除非他想到可能存在这样的函数并且他检查了文档,并且文档是完整且易于理解的?我宁愿拥有一个总是给我一个有意义的响应的函数,而不是两个我必须知道并记住调用两者的函数。
【讨论】:
为什么必须将“无应答”和“零”响应组合在同一个返回值中?取而代之的是,让该方法返回“过去五年内的任何以前的住院情况”,并有一个单独的方法询问“以前的住院名单是否填写过?”。这是假设填写的未曾住院的名单与未填写的名单之间存在差异。 但是如果你返回 null 你已经给调用者增加了额外的负担!调用者必须检查每个返回值是否为空——这是一个可怕的 DRY 违规。如果您想单独指出“调用者没有回答问题”,那么创建一个额外的方法调用来指示事实会更简单。要么,要么使用具有 DeclinedToAnswer 额外属性的派生集合类型。永不返回 null 的规则根本不是任意的,它是最小意外原则。此外,你的方法的返回类型应该是它的名字所说的。 Null 几乎可以肯定不会。 您假设您的 getHospitalizationList 仅从一个地方调用和/或所有调用者都希望区分“无应答”和“零”情况。 将在某些情况下(几乎可以肯定是大多数情况),您的调用者不需要进行这种区分,因此您强迫他们在不需要的地方添加空检查。这给您的代码库增加了很大的风险,因为人们很容易忘记这样做 - 并且因为返回 null 而不是集合的合法理由很少,所以这种情况更有可能发生。 RE 名称:没有函数名称可以完全描述函数的作用,除非它与函数一样长,因此非常不切实际。但无论如何,如果函数在没有给出答案的情况下返回一个空列表,那么按照同样的推理,它不应该被称为“getHospitalizationListOrEmptyListIfNoAnswer”吗?但实际上,您会坚持将 Java Reader.read 函数重命名为 readCharacterOrReturnMinusOneOnEndOfStream 吗?那 ResultSet.getInt 真的应该是“getIntOrZeroIfValueWasNull”吗?等等。 RE 每个调用都想区分:嗯,是的,我假设,或者至少调用者的作者应该做出他不关心的有意识的决定。如果函数返回一个“不知道”的空列表,而调用者盲目地对待这个“无”,它可能会给出严重不准确的结果。想象一下,如果函数是“getAllergicReactionToMedicationList”。盲目地将“未输入列表”视为“患者没有已知的过敏反应”的程序可能会导致患者死亡。在许多其他系统中,您会得到类似的结果,但结果可能不那么显着。 ...【参考方案7】:我喜欢在这里给出适当的例子。
在这里考虑一个案例..
int totalValue = MySession.ListCustomerAccounts()
.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID)
.Sum(account => account.AccountValue);
这里考虑一下我正在使用的功能..
1. ListCustomerAccounts() // User Defined
2. FindAll() // Pre-defined Library Function
我可以轻松地使用ListCustomerAccount
和FindAll
代替。,
int totalValue = 0;
List<CustomerAccounts> custAccounts = ListCustomerAccounts();
if(custAccounts !=null )
List<CustomerAccounts> custAccountsFiltered =
custAccounts.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID );
if(custAccountsFiltered != null)
totalValue = custAccountsFiltered.Sum(account =>
account.AccountValue).ToString();
注意:由于 AccountValue 不是 null
,Sum() 函数不会
返回null
.,这样我就可以直接使用了。
【讨论】:
【参考方案8】:取决于您的合同和您的具体案例。 通常最好返回空集合,但有时(很少):
null
可能意味着更具体的东西;
您的 API(合同)可能会强制您返回 null
。
一些具体的例子:
UI 组件(来自您无法控制的库)如果传递的是空集合,则可能呈现一个空表,或者如果传递的是 null,则根本没有表。 在 Object-to-XML (JSON/whatever) 中,其中null
表示元素丢失,而空集合将呈现冗余(并且可能不正确)<collection />
您正在使用或实现一个明确声明应返回/传递 null 的 API
【讨论】:
@Bozho- 这里有一些有趣的例子。 我有点明白你的意思,尽管我认为你不应该围绕其他错误构建代码,并且根据 c# never 中的定义“null”意味着特定的东西。值 null 被定义为“无信息”,因此说它携带特定信息是矛盾的。这就是为什么 .NET 指南规定如果集合确实为空,则应返回一个空集。返回 null 是说:“我不知道预期的集合去了哪里” 不,它的意思是“没有集合”而不是“集合没有元素” 我以前也这么认为,后来我写了很多TSQL,结果发现并不总是这样,呵呵。 Object-To-Xml 的部分在【参考方案9】:Empty 对消费者更友好。
有一个明确的方法可以组成一个空的枚举:
Enumerable.Empty<Element>()
【讨论】:
感谢您的巧妙技巧。我像个白痴一样实例化了一个空的 List始终为您的客户(正在使用您的 api)着想:
返回“null”通常会导致客户端无法正确处理 null 检查,这会在运行时导致 NullPointerException。我见过这样一个缺失的空检查强制优先生产问题的情况(客户端使用 foreach(...) 空值)。在测试过程中没有出现问题,因为操作的数据略有不同。
【讨论】:
【参考方案11】:有人可能会争辩说,Null Object Pattern 背后的原因类似于支持返回空集合的原因。
【讨论】:
【参考方案12】:还有一点尚未提及。考虑以下代码:
public static IEnumerable<string> GetFavoriteEmoSongs()
yield break;
调用此方法时,C# 语言将返回一个空枚举器。因此,为了与语言设计(以及程序员的期望)保持一致,应该返回一个空集合。
【讨论】:
从技术上讲,我不认为这会返回一个空集合。 @FryGuy - 不,它没有。它返回一个 Enumerable 对象,其 GetEnumerator() 方法返回一个空的 Enumerator(通过与集合的类比)。也就是说,Enumerator 的 MoveNext() 方法总是返回 false。调用示例方法不会返回 null,也不会返回 GetEnumerator() 方法返回 null 的 Enumerable 对象。【参考方案13】:我认为null
与空集合不同,您应该选择最能代表您返回的内容的集合。在大多数情况下,null
什么都不是(SQL 除外)。一个空的集合是一些东西,尽管是一个空的东西。
如果你必须选择其中一个,我会说你应该倾向于一个空集合而不是空集合。但有时空集合与空值不同。
【讨论】:
【参考方案14】:视情况而定。如果是特殊情况,则返回null。如果函数恰好返回一个空集合,那么显然返回是可以的。但是,由于参数无效或其他原因,将空集合作为特殊情况返回并不是一个好主意,因为它掩盖了特殊情况。
实际上,在这种情况下,我通常更喜欢抛出异常以确保它真的不被忽略:)
说它使代码更健壮(通过返回一个空集合)因为它们不必处理 null 条件是不好的,因为它只是掩盖了应该由调用代码处理的问题。
【讨论】:
【参考方案15】:空集合。如果您使用 C#,则假设最大化系统资源不是必需的。虽然效率较低,但返回 Empty Collection 对所涉及的程序员来说更方便(原因将在上面概述)。
【讨论】:
【参考方案16】:返回 null 可能更有效,因为不会创建新对象。但是,它通常还需要null
检查(或异常处理)。
从语义上讲,null
和一个空列表并不意味着同一件事。差异是微妙的,在特定情况下,一种选择可能比另一种更好。
无论您如何选择,都要记录下来以避免混淆。
【讨论】:
在考虑 API 设计的正确性时,效率几乎永远不会成为一个因素。在一些非常具体的情况下,例如图形基元,可能是这样,但是在处理列表和大多数其他高级事物时,我非常怀疑。 同意 Greg 的观点,特别是考虑到 API 用户为补偿这种“优化”而编写的代码可能比一开始就使用更好的设计效率低。 同意,在大多数情况下,它根本不值得优化。借助现代内存管理,空列表实际上是免费的。【参考方案17】:大约一周前,我们在工作的开发团队中进行了讨论,我们几乎一致认为是空集合。一个人想返回 null 的原因与上面 Mike 指定的相同。
【讨论】:
【参考方案18】:如果一个空集合在语义上有意义,那是我更愿意返回的。为 GetMessagesInMyInbox()
返回一个空集合表示“您的收件箱中确实没有任何消息”,而返回 null
可能有助于表示没有足够的数据来说明可能返回的列表应该是什么样子.
【讨论】:
在您给出的示例中,听起来如果该方法无法满足请求而不是仅返回 null,则该方法可能应该抛出异常。异常对于诊断问题比空值有用得多。 嗯,是的,在收件箱示例中,null
的值肯定看起来不合理,我在用更笼统的术语思考那个。异常也可以很好地传达出现问题的事实,但是如果完全可以预期所指的“数据不足”,那么抛出异常将是糟糕的设计。我宁愿考虑一种情况,该方法有时无法计算响应是完全有可能的并且根本没有错误。以上是关于返回 null 或空集合更好吗?的主要内容,如果未能解决你的问题,请参考以下文章