返回空集合还是空集合更好?

这是一个一般性的问题(但我使用c#),最好的方法(最佳实践)是什么,对于一个有集合作为返回类型的方法,你返回null集合还是空集合?

140088 次浏览

空集合。总是这样。

这糟透了:

if(myInstance.CollectionProperty != null)
{
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
}

在返回集合或枚举对象时,NEVER返回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>()可以被视为比返回一个新的空集合或数组更有效。

返回null可能更有效,因为不会创建新的对象。然而,它也经常需要null检查(或异常处理)。

从语义上讲,null和一个空列表并不意味着同一件事。这些差异是微妙的,在特定的情况下,一种选择可能比另一种更好。

不管你的选择是什么,记录下来以避免混淆。

取决于你的合同具体的案例。 通常是最好返回空集合,但有时(很少):

  • null可能意味着更具体的东西;
  • 你的API(合约)可能会强制你返回null

一些具体的例子:

  • 一个UI组件(来自一个不受你控制的库),如果传递一个空集合,可能会呈现一个空表,或者如果传递null,则根本不呈现表。
  • 在对象到xml (JSON/其他)中,其中null意味着元素缺失,而空集合将呈现冗余的(可能不正确的)<collection />
  • 你正在使用或实现一个明确声明null应该返回/传递的API

如果一个空集合在语义上是有意义的,那就是我喜欢返回的。返回GetMessagesInMyInbox()的空集合表示“您的收件箱中确实没有任何消息”,而返回null可能用于表示可用数据不足,无法说明可能返回的列表应该是什么样子。

大约一周前,我们在开发团队中讨论过这个问题,我们几乎一致同意采用空收集。有人想要返回null,原因与Mike上面指定的相同。

空集合。如果您正在使用c#,那么假定最大化系统资源并不是必要的。虽然效率较低,但返回Empty Collection对于所涉及的程序员来说要方便得多(出于Will在上面概述的原因)。

框架设计指南第二版(第256页):

不返回空值 集合属性或方法 返回集合。返回空

这是另一篇关于不返回null的好处的有趣文章(我试图在Brad Abram的博客上找到一些东西,他链接到了这篇文章)。

编辑-正如Eric Lippert现在对原始问题的评论,我还想链接到他的优秀文章

视情况而定。如果是特殊情况,则返回null。如果函数恰好返回一个空集合,那么返回显然是可以的。但是,由于无效参数或其他原因而返回一个空集合作为特殊情况并不是一个好主意,因为它掩盖了一个特殊情况条件。

实际上,在这种情况下,我通常更喜欢抛出一个异常,以确保它真的没有被忽略:)

说它使代码更健壮(通过返回一个空集合),因为它们不必处理null条件是不好的,因为它只是掩盖了应该由调用代码处理的问题。

我认为null并不等同于一个空集合,你应该选择哪一个最能代表你要返回的内容。在大多数情况下null什么都不是(在SQL中除外)。空的收藏是一种东西,尽管是一种空的东西。

如果你不得不选择一个或另一个,我会说你应该倾向于一个空集合而不是null。但有时空集合与空值并不相同。

还有一点尚未被提及。考虑下面的代码:

    public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}

在调用此方法时,c#语言将返回一个空枚举数。因此,为了与语言设计(以及程序员的期望)保持一致,应该返回一个空集合。

有人可能会说,空对象模式背后的原因与支持返回空集合的原因类似。

在我看来,您应该返回上下文中语义正确的值,不管它是什么。“总是返回一个空集合”的规则对我来说似乎有点简单。

假设,在一个医院的系统中,我们有一个函数,它应该返回过去5年所有以前住院的列表。如果客户没有去过医院,那么返回一个空列表是很有意义的。但是,如果顾客把准入表的那部分空着怎么办?我们需要一个不同的值来区分“空列表”和“无答案”或“不知道”。我们可以抛出异常,但它不一定是错误条件,也不一定会使我们脱离正常的程序流程。

我经常对无法区分零和无答案的系统感到沮丧。我曾多次遇到这样的情况:系统要求我输入一些数字,我输入0,然后我得到一个错误消息,告诉我必须在这个字段中输入一个值。我刚刚做了:我输入了零!但它不会接受0,因为它无法区分0和no。


回复桑德斯:

是的,我认为“这个人没有回答这个问题”和“答案是零”是有区别的。这就是我回答的最后一段的要点。许多程序无法区分“不知道”与空白或零,这在我看来是一个潜在的严重缺陷。例如,大约一年前,我在买房子。我上了一个房地产网站,上面有很多房子的要价都是0美元。听起来很不错:他们免费赠送这些房子!但我敢肯定,可悲的现实是,他们没有进入价格。在这种情况下,你可能会说,“显然,零意味着他们没有进入价格——没有人会免费赠送房子。”但该网站还列出了各个城镇房屋的平均要价和售价。我不禁怀疑平均数是否不包括零,因此给了一些地方一个不正确的低平均数。例如,10万美元的平均值是多少;120000美元;“不知道”呢?从技术上讲,答案是“不知道”。我们真正想看到的可能是11万美元。但我们得到的可能是73333美元,这是完全错误的。另外,如果我们在用户可以在线订购的网站上遇到这个问题怎么办?(对房地产来说不太可能,但我相信你在许多其他产品上都见过这种情况。)我们真的希望“价格尚未明确”被理解为“免费”吗?

RE有两个独立的函数,一个是“有吗?”,一个是“如果有,是什么?”是的,你当然可以这么做,但你为什么要这么做呢?现在调用程序必须进行两次调用而不是一次。如果程序员没有调用“any?”而直接调用“What is it?”会发生什么?? 程序会返回一个误导的零吗?抛出异常?返回一个未定义的值?这会产生更多的代码、更多的工作和更多的潜在错误。

我看到的唯一好处是,它使您能够遵守任意规则。这条规则有什么好处,值得我们不费苦心地遵守吗?如果不是,为什么要麻烦呢?


回复Jammycakes:

考虑一下实际的代码会是什么样子。我知道问题说的是c#,但如果我写Java的话,请原谅。我的c#不是很清晰,原理是一样的。

返回null:

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,程序可能会出现空指针异常而失败。但如果有一个单独的“got any”函数,而调用者不调用该函数,而是盲目地调用“get list”函数,那么会发生什么?如果它抛出异常或以其他方式失败,那么,这与它返回null并且不检查它所发生的情况几乎相同。如果它返回一个空列表,那是错误的。你无法区分“我有一个零元素的列表”和“我没有一个列表”。这就像当用户没有输入任何价格时,价格返回0:这是错误的。

我看不出向集合附加附加属性有什么帮助。打电话的人仍然需要检查。这比检查null好吗?同样,最糟糕的情况是程序员忘记检查,并给出不正确的结果。

如果程序员熟悉null意味着“没有值”的概念,那么函数返回null就不足为奇了,我认为任何有能力的程序员都应该听说过这个概念,不管他是否认为这是一个好主意。我认为拥有一个独立的功能更像是一个“惊喜”问题。如果程序员不熟悉API,当他在没有数据的情况下运行测试时,他很快就会发现有时他得到的结果是null。但是他怎么发现另一个函数的存在呢?除非他突然想到可能有这样一个函数,他检查了文档,文档是完整的和可理解的?我宁愿有一个函数,总是给我一个有意义的响应,而不是两个函数,我必须知道并记住调用这两个函数。

总是为你的客户端着想(他们正在使用你的api):

返回'null'通常会导致客户端不能正确处理null检查的问题,这会导致运行时出现NullPointerException异常。我曾经见过这样的情况,缺少空检查会导致优先级生产问题(客户端对空值使用foreach(…))。在测试期间,问题没有发生,因为所操作的数据略有不同。

空的对消费者更友好。

有一个明确的方法来创建一个空的枚举:

Enumerable.Empty<Element>()

我喜欢在这里用适当的例子来解释。

考虑一个案例。

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

我可以很容易地使用ListCustomerAccountFindAll来代替。

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返回。,因此我可以直接使用它。

在大多数情况下,返回空集合更好。

这样做的原因是调用方的实现方便,契约一致,实现更容易。

如果一个方法返回null表示空结果,调用者必须实现一个空检查适配器,除了枚举。 这段代码随后在不同的调用者中复制,所以为什么不把这个适配器放在方法中,这样它就可以重用了

IEnumerable有效使用null可能表示没有结果,或者操作失败,但在这种情况下,应该考虑使用其他技术,例如抛出异常。

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;


namespace StackOverflow.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="IEnumerable{T}"/> 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="IEnumerable{T}"/> 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="IEnumerable{T}"/> 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://stackoverflow.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.Count{TSource}(System.Collections.Generic.IEnumerable{TSource})"/>
/// </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.First{TSource}(System.Collections.Generic.IEnumerable{TSource})"/> 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());
}
}
}
那时,我正在用面向对象语言设计第一个用于引用的综合类型系统。我的目标是确保所有引用的使用都是绝对安全的,并由编译器自动执行检查。但我无法抗拒加入空引用的诱惑,原因很简单,因为它很容易实现。这导致了数不清的错误、漏洞和系统崩溃,在过去40年里可能造成了10亿美元的痛苦和损失。 ——托尼·霍尔,ALGOL W的发明者。 < / p >

关于null的详细描述见在这里。我不同意undefined是另一个null的说法,但它仍然值得一读。它解释了为什么你应该避免null,而不仅仅是在你问的情况下。其本质是,null在任何语言中都是一种特殊情况。你必须将null视为一个异常。undefined在这方面是不同的,处理未定义行为的代码在大多数情况下只是一个bug。C和大多数其他语言也有未定义的行为,但它们中的大多数在语言中没有标识符。

从管理复杂性(一个主要的软件工程目标)的角度来看,我们希望避免将不必要的圈复杂度传播到API的客户端。向客户端返回null就像返回另一个代码分支的圈复杂度成本。

(这对应于单元测试负担。除了空集合返回情况外,您还需要为空返回情况编写一个测试。)

Go似乎是首选nil而不是空数组的语言。

https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices

当声明一个空片时,首选var t []string而不是t := []string{}。前者声明了一个nil切片值,而后者不是-nil而是零长度。它们在功能上是等价的——它们的lencap都是0——但是nil片是首选样式。