返回null或空集合是否更好?

时间:2009-12-28 15:30:29

标签: c# collections

这是一个普遍的问题(但我使用的是C#),最好的方法(最佳实践)是什么,对于将集合作为返回类型的方法返回null或空集合?

18 个答案:

答案 0 :(得分:457)

空集合。总是

这很糟糕:

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

答案 1 :(得分:148)

来自Framework Design Guidelines 2nd Edition(第256页):

  

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

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

编辑 - 正如Eric Lippert现在对原始问题发表评论,我也想link to his excellent article

答案 2 :(得分:86)

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

  • null可能意味着更具体的内容;
  • 您的API(合约)可能会强制您返回null

一些具体的例子:

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

答案 3 :(得分:36)

还有一点尚未提及。请考虑以下代码:

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

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

答案 4 :(得分:30)

空对消费者更友好。

有一种明确的方法可以构成一个空的可枚举:

Enumerable.Empty<Element>()

答案 5 :(得分:18)

答案 6 :(得分:10)

如果空集合在语义上有意义,那就是我更喜欢返回的。返回GetMessagesInMyInbox()的空集合表示“您的收件箱中确实没有任何消息”,而返回null可能有助于表明可用的数据不足以说明可能返回的列表应该看起来像。

答案 7 :(得分:6)

返回null可能更有效,因为没有创建新对象。但是,它通常还需要null检查(或异常处理。)

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

无论您的选择如何,请记录下来以避免混淆。

答案 8 :(得分:6)

有人可能会说Null Object Pattern背后的推理类似于赞成返回空集合的推理。

答案 9 :(得分:4)

我认为null与空集合不同,你应该选择哪一个最能代表你要归还的东西。在大多数情况下,null没什么(除了在SQL中)。空集合虽然是空洞的东西。

如果你必须选择其中一个,我会说你应该倾向于空集合而不是null。但有时空集合与空值不同。

答案 10 :(得分:4)

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

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

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

答案 11 :(得分:4)

始终考虑您的客户(使用您的API):

返回'null'经常会导致客户端无法正确处理空检查的问题,这会在运行时导致NullPointerException。我已经看到这样一种情况,其中这样的遗漏空检查强制优先生产问题(客户端在空值上使用foreach(...))。在测试期间,问题没有发生,因为操作的数据略有不同。

答案 12 :(得分:3)

我想在这里给出解释,并提供合适的例子。

在这里考虑一个案例..

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

答案 13 :(得分:2)

空集合。如果您使用C#,则假设最大化系统资源并非必不可少。虽然效率较低,但返回空集合对于所涉及的程序员来说更加方便(原因如上所述)。

答案 14 :(得分:2)

我们在开发团队工作了一周左右的时间,我们几乎一致地进行了空集。一个人想要返回null,原因与上面Mike指出的相同。

答案 15 :(得分:2)

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

这样做的原因是方便实现调用者,一致的合同和更容易的实现。

如果方法返回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());
        }
    }
}

答案 16 :(得分:2)

  

我称之为十亿美元的错误......那时,我正在设计第一个用于面向对象语言的引用的综合类型系统。我的目标是确保所有引用的使用绝对安全,并由编译器自动执行检查。但是我无法抗拒引入空引用的诱惑,仅仅因为它很容易实现。这导致了无数的错误,漏洞和系统崩溃,这可能在过去四十年中造成了数十亿美元的痛苦和损害。    - ALGOL W的发明者Tony Hoare

有关null的详细信息,请参阅here。我不同意undefined是另一个null的陈述,但仍然值得一读。它解释了,为什么你应该完全避免使用null,而不仅仅是你所要求的情况。实质是,null在任何语言中都是特例。您必须将null视为例外。 undefined以这种方式不同,处理未定义行为的代码在大多数情况下只是一个错误。 C和大多数其他语言也有未定义的行为,但大多数语言都没有该语言的标识符。

答案 17 :(得分:1)

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

(这与单元测试负担相对应。除了空集合返回案例外,您还需要针对空返回案例编写测试。)