返回null还是空集合更好?

这是一个普遍的问题(但我正在使用C#),什么是最好的方法(最佳实践),你是否返回null或空集合的方法有一个集合作为返回types?

空集合。 总是。

这很糟糕:

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

返回集合或枚举时,不要返回null总是返回一个空的枚举/集合。 它可以防止上述的废话,并防止你的车被你的class级的同事和用户怂恿。

在谈论属性时,一定要设置属性,忘记它

 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>()可以看作比返回更有效,例如,一个新的空集合或数组。

从框架devise指南第二版 (第256页):

不要从集合属性或返回集合的方法返回空值。 而是返回一个空集合或一个空数组。

这里有另外一篇关于不返回空值的好处的文章(我试图在Brad Abram的博客上find一些东西,而且他把这篇文章链接到了这篇文章中)。

编辑 –正如Eric Lippert现在对原始问题所做的评论,我也想链接到他的优秀文章 。

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

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

一些具体的例子:

  • 一个UI组件(来自你的控件之外的一个库),如果传递了一个空的集合,可能会渲染一个空的表,或者如果传递的是null,则根本没有表。
  • 在一个对象到XML(JSON /无论),其中null将意味着该元素丢失,而一个空集合将呈现一个冗余(可能是不正确的) <collection />
  • 你正在使用或实现一个明确指出null应该被返回/传递的API

还有一点还没有提到。 考虑下面的代码:

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

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

空更加消费者友好。

有一个明确的方法来构造一个空的枚举:

 Enumerable.Empty<Element>() 

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

假设在一个医院系统中,我们有一个function,应该返回过去5年所有以前住院的清单。 如果客户没有住院,那么返回一个空的列表是很有意义的。 但是,如果客户将这部分入场表格留空,该怎么办? 我们需要一个不同的价值来区分“空列表”和“不回答”或“不知道”。 我们可以抛出一个exception,但这不一定是错误的情况,也不一定会把我们赶出正常的程序stream程。

我常常因无法区分零和无答案的系统而感到沮丧。 我有很多次系统要求我input一个数字,我input零,我得到一个错误信息告诉我,我必须在这个字段中input一个值。 我刚刚做到了:我input了零! 但它不会接受零,因为它不能区分它和没有答案。


回复桑德斯:

是的,我假设“人没有回答这个问题”和“答案是零”有区别。 这是我答案最后一段的重点。 很多程序无法区分“不知道”从空白还是零,这似乎是一个潜在的严重缺陷。 例如,一年前我在买房子。 我去了一个房地产网站,有很多房子上市,要价$ 0。 听起来不错,他们把这些房子免费送走! 但我确定,可悲的现实是,他们只是没有进入价格。 在这种情况下,你可能会说:“明显地,零表示他们没有进入价格 – 没有人会免费送走一间房子”。 但该网站还列出了各镇住房的平均要价和销售价格。 我不禁想知道平均值是否包含零点,从而给一些地方的平均值偏低。 即10万美元的平均数是多less; $ 120,000; 和“不知道”? 技术上的答案是“不知道”。 我们可能真正想看的是11万美元。 但是我们可能得到的是73333美元,这是完全错误的。 另外,如果我们在用户可以在线订购的网站上遇到这个问题呢? (房地产不太可能,但我相信你已经看到了很多其他产品。)我们真的想要“价格尚未确定”被解释为“免费”吗?

RE有两个独立的function,一个“有没有? 和“如果是,那是什么?” 是的,你当然可以做到这一点,但你为什么要? 现在调用程序必须做两个调用,而不是一个。 如果程序员不能调用“any”,会发生什么? 并直接向“这是什么?” ? 程序是否会返回错误的零? 抛出exception? 返回一个未定义的值? 它会创build更多的代码,更多的工作和更多的潜在错误。

我看到的唯一好处是它可以让你遵守任意的规则。 这个规则有没有什么好处,让它值得服从它的麻烦? 如果没有,为什么要麻烦?


回复Jammycakes:

考虑一下实际的代码是什么样的。 我知道C#的问题,但如果我编写Java,请原谅我。 我的C#不是非常尖锐的原则是一样的。

用null返回:

 HospList list=patient.getHospitalizationList(patientId); if (list==null) { // ... handle missing list ... } else { for (HospEntry entry : list) // ... do whatever ... } 

具有单独的function:

 if (patient.hasHospitalizationList(patientId)) { // ... handle missing list ... } else { HospList=patient.getHospitalizationList(patientId)) for (HospEntry entry : list) // ... do whatever ... } 

它实际上是一个或两个代码与空返回,所以它不是更多的负担,更less的调用者。

我不明白它是如何造成干的问题。 这不像我们必须执行两次电话。 如果我们总是想在列表不存在的情况下做同样的事情,也许我们可以把处理下移到get-list函数,而不是让调用者这样做,所以把代码放在调用者中将是DRY违规。 但是我们几乎肯定不想总是做同样的事情。 在我们必须处理列表的函数中,缺less的列表是一个可能会导致处理停止的错误。 但是在编辑屏幕上,如果还没有input数据,我们肯定不想停止处理:我们要让它们input数据。 所以处理“没有列表”必须以这样或那样的方式在呼叫者层面上完成。 而且,我们是用一个空归还还是一个单独的函数来做到这一点,对于更大的原则没有任何影响。

当然,如果调用者不检查null,程序可能会失败并出现空指针exception。 但是,如果有一个单独的“有任何”的function,调用者不调用该函数,但盲目地调用“获取列表”function,那么会发生什么? 如果它抛出一个exception或者失败了,那么,如果它返回null并且没有检查它将会发生什么。 如果它返回一个空列表,那就错了。 你没有区分“我有一个零元素列表”和“我没有一个列表”。 这就好像用户没有进入任何价格时的价格为零:这是错误的。

我没有看到如何附加一个额外的属性收集帮助。 来电者仍然需要检查。 如何比检查null更好? 再一次,绝对最糟糕的事情是程序员忘了检查它,并给出不正确的结果。

如果程序员熟悉空值意思是“没有价值”的概念,那么返回null的函数并不令人意外,我认为任何有能力的程序员都应该听说过这个概念,不pipe他认为这是一个好主意。 我觉得有一个单独的function更多是一个“惊喜”的问题。 如果一个程序员不熟悉这个API,当他运行一个没有数据的testing时,他会很快发现有时候他会得到一个空值。 但是他怎么会发现另一个function的存在呢,除非他觉得可能有这样的function,他检查文档,文档是完整的和可理解的? 我宁愿有一个function,总是给我一个有意义的回应,而不是我必须知道和记得调用两个function。

如果一个空集合在语义上是有意义的,那就是我喜欢返回的东西。 为GetMessagesInMyInbox()返回一个空的集合来传递“你真的没有在你的收件箱中有任何消息”,而返回null可能是有用的沟通,说没有足够的数据可以说可能返回的列表应该看起来像。

人们可以争辩说, 空对象模式背后的推理类似于赞成返回空集合的推理。

返回null可能更有效,因为不会创build新的对象。 但是,它也经常需要一个null检查(或exception处理)。

在语义上, null和一个空列表并不意味着同样的事情。 差异是微妙的,在特定情况下,一个select可能比另一个更好。

无论您select什么,都要logging在案以避免混淆。

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

其实,在这种情况下,我通常更喜欢抛出exception,以确保它是真的不会被忽略:)

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

我会争辩说, null与空集合不同,你应该select哪一个最能代表你正在返回的东西。 在大多数情况下, null是什么(除了在SQL中)。 一个空的集合是一些东西,虽然是空的东西。

如果你必须select一个或另一个,我会说你应该倾向于一个空的集合,而不是空的。 但有时候,空集合与空值不是一回事。

总是有利于你的客户(使用你的API):

返回'null'通常会导致客户端无法正确处理空值检查的问题,这会在运行时导致NullPointerException。 我已经看到了这样一个缺less空检查的情况下强制优先生产问题(客户端使用foreach(…)在空值)。 在testing过程中没有发生问题,因为操作的数据稍有不同。

我喜欢用适当的例子来解释一下。

考虑这里的情况..

 int totalValue = MySession.ListCustomerAccounts() .FindAll(ac => ac.AccountHead.AccountHeadID == accountHead.AccountHeadID) .Sum(account => account.AccountValue); 

这里考虑我正在使用的function..

 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,这与Mike上面指出的原因是一样的。

空集合。 如果您使用的是C#,则假定系统资源最大化并不重要。 虽然效率较低,但返回空集合对于程序员来说更为方便(因为上面会列出)。

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

这是因为呼叫方的实施方便,合同一致,执行起来也比较容易。

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

对于IEnumerable而言,null的有效用法可能是缺less结果或操作失败的指示,但在这种情况下应该考虑其他技术,例如抛出exception。

 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()); } } }