关于C#:返回null 还是 empty collection更好?

Is it better to return null or empty collection?

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


空集合。总是。

这很糟糕:

1
2
3
4
5
if(myInstance.CollectionProperty != null)
{
  foreach(var item in myInstance.CollectionProperty)
    /* arrgh */
}

在返回集合或可枚举集合时,最好不要返回null。始终返回空的可枚举/集合。它可以防止上述的胡说八道,也可以防止你的汽车受到同事和你所在班级的用户的怂恿。

当谈论属性时,总是设置一次属性,然后忘记它。

1
2
3
public List<Foo> Foos {public get; private set;}

public Bar() { Foos = new List<Foo>(); }

在.NET 4.6.1中,您可以将其浓缩很多:

1
public List<Foo> Foos { get; } = new List<Foo>();

当讨论返回可枚举的方法时,您可以轻松地返回空的可枚举的,而不是返回null

1
2
3
4
public IEnumerable<Foo> GetMyFoos()
{
  return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}

使用Enumerable.Empty()比返回新的空集合或数组更有效。


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

DO NOT return null values from
collection properties or from methods
returning collections. Return an empty
collection or an empty array instead.

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

编辑-正如埃里克·利珀特现在对最初的问题发表的评论,我还想链接到他优秀的文章。


取决于你的合同和具体情况。一般来说,最好返回空集合,但有时(很少):

  • null可能意味着更具体的东西;
  • 您的API(合同)可能会迫使您返回null

一些具体例子:

  • 如果传递的是空集合,则UI组件(来自您无法控制的库)可能呈现空表;如果传递的是空集合,则可能根本没有表。
  • 在XML对象(json/whatever)中,null表示元素丢失,而空集合则表示多余(可能不正确)
  • 您正在使用或实现一个API,该API显式声明应返回/传递空值


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

1
2
3
4
    public static IEnumerable<string> GetFavoriteEmoSongs()
    {
        yield break;
    }

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


空的对消费者更友好。

有一个明确的方法来弥补一个空的可枚举:

1
Enumerable.Empty<Element>()


在我看来,您应该返回上下文中语义正确的值,不管它是什么。在我看来,"总是返回空集合"的规则有点简单。好的。

假设在一个医院的系统中,我们有一个功能,该功能应该返回过去5年中所有以前住院的列表。如果客户不在医院,则返回空列表是很有意义的。但是,如果客户把准入表的那部分留空了呢?我们需要一个不同的值来区分"空列表"和"没有答案"或"不知道"。我们可以抛出一个异常,但它不一定是一个错误条件,也不一定会把我们从正常的程序流中赶出来。好的。

我经常被无法区分零和无答案的系统所挫败。我有很多次系统要求我输入一些数字,我输入零,然后我收到一条错误消息,告诉我必须在这个字段中输入一个值。我刚刚做了:我输入了零!但它不会接受零,因为它不能区分它和没有答案。好的。

回复桑德斯:好的。

是的,我假设"某人没有回答问题"和"答案是零"之间存在差异,这就是我答案的最后一段。许多程序无法区分"不知道"和空白或零,在我看来这是一个潜在的严重缺陷。例如,一年前我在买房子。我去了一个房地产网站,那里有许多房子以0美元的要价上市。听起来不错:他们免费赠送这些房子!但我敢肯定,令人悲伤的现实是,他们只是没有进入价格。在这种情况下,你可能会说,"显然,零意味着他们没有输入价格——没有人会免费赠送一套房子。"但网站也列出了各个城镇的平均房屋买卖价格。我忍不住想知道平均值是否不包括零,因此在某些地方给出了一个错误的低平均值。也就是说,10万美元、12万美元和"不知道"的平均值是多少?从技术上讲,答案是"不知道"。我们可能真正想看到的是11万美元。但我们可能会得到73333美元,这是完全错误的。另外,如果我们在一个用户可以在线订购的站点上遇到这个问题怎么办?(不太可能是房地产,但我相信你已经看到了很多其他产品的情况。)我们真的希望"价格尚未明确"被解释为"免费"吗?好的。

有两个独立的功能,"有吗?"还有一个"如果是,那是什么?"是的,你当然可以这样做,但你为什么要这样做?现在呼叫程序必须进行两次呼叫而不是一次。如果程序员不能调用"any",会发生什么?直接进入"这是什么?"程序是否会返回错误的前导零?是否引发异常?返回未定义的值?它会创建更多的代码、更多的工作和更多的潜在错误。好的。

我看到的唯一好处是它使您能够遵守任意规则。这条规则有什么好处使它值得费心去遵守它吗?如果没有,何必麻烦?好的。

回复jammycakes:好的。

考虑实际代码的外观。我知道这个问题,C说,但是如果我写Java,请原谅。我的C不是很锋利,原理是一样的。好的。

返回空值:好的。

1
2
3
4
5
6
7
8
9
10
HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
   // ... handle missing list ...
}
else
{
  for (HospEntry entry : list)
   //  ... do whatever ...
}

具有单独的功能:好的。

1
2
3
4
5
6
7
8
9
10
if (patient.hasHospitalizationList(patientId))
{
   // ... handle missing list ...
}
else
{
  HospList=patient.getHospitalizationList(patientId))
  for (HospEntry entry : list)
   // ... do whatever ...
}

它实际上是一行或两行更少的带有空返回的代码,所以对调用者来说并不是更大的负担,而是更少的负担。好的。

我不明白它是如何造成一个干燥的问题的。我们不需要执行两次调用。如果我们总是想在列表不存在的情况下做同样的事情,也许我们可以将处理向下推到get list函数,而不是让调用者做,因此将代码放入调用者中是一种完全违反规则的行为。但我们几乎肯定不想总是做同样的事情。在必须要处理列表的函数中,缺少列表是一个错误,很可能会停止处理。但是在编辑屏幕上,如果他们还没有输入数据,我们肯定不想停止处理:我们想让他们输入数据。因此,处理"无列表"必须以某种方式在调用方级别完成。不管我们是用一个空返回还是一个单独的函数来实现这一点,对于更大的原则来说都没有什么区别。好的。

当然,如果调用者不检查空值,程序可能会因空指针异常而失败。但是如果有一个单独的"got any"函数,调用方不调用该函数,而是盲目调用"get list"函数,那么会发生什么?如果它抛出了一个异常或者失败了,那么,这与如果它返回空值而不检查它会发生的情况差不多。如果它返回一个空列表,那就是错误的。你无法区分"我有一个零元素列表"和"我没有列表"。这就像用户没有输入任何价格时返回零价格一样:这是错误的。好的。

我看不到向集合附加附加属性有什么帮助。打电话的人还是要检查一下。这比检查空值更好吗?同样,可能发生的最糟糕的事情是程序员忘记检查它,并给出错误的结果。好的。

如果程序员熟悉空的概念,即"没有值",我认为任何一个称职的程序员都应该听说过,不管他认为这是不是一个好主意,返回空的函数并不令人惊讶。我认为有一个单独的功能更像是一个"惊喜"的问题。如果程序员不熟悉API,当他在没有数据的情况下运行测试时,他会很快发现有时他会得到一个空值。但是,他如何发现另一个函数的存在,除非他想到可能存在这样一个函数,并且他检查了文档,并且文档是完整的和可理解的?我宁愿有一个函数总是给我一个有意义的响应,而不是两个函数,我必须知道并记住调用这两个函数。好的。好啊。


如果一个空集合在语义上是有意义的,那么我宁愿返回这个集合。返回一个空的GetMessagesInMyInbox()集合可以传递"你的收件箱里真的没有任何消息",而返回null可能有助于传递没有足够的数据来说明返回的列表应该是什么样子。


返回空值可能更有效,因为没有创建新对象。然而,它也常常需要一个null检查(或异常处理)。

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

无论您选择什么,都要记录下来以避免混淆。


可以说,空对象模式背后的推理类似于返回空集合的推理。


我想说的是,null与空的集合不是一回事,你应该选择最能代表你要返回的集合。在大多数情况下,null不是什么(除了在SQL中)。一个空的集合是一些东西,尽管是一个空的东西。

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


始终以支持您的客户(使用您的API的客户)为己任:

返回"null"通常会导致客户端无法正确处理空检查,这会导致运行时出现NullPointerException。我曾经看到过这样的情况:这种缺少空检查会强制产生优先级生产问题(客户机在空值上使用foreach(…)。在测试过程中没有出现问题,因为操作的数据略有不同。


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

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

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


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

这里考虑一个案例……

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

这里考虑一下我使用的函数。

1
2
1. ListCustomerAccounts() // User Defined
2. FindAll()              // Pre-defined Library Function

我可以很容易地使用ListCustomerAccountFindAll,而不是。

1
2
3
4
5
6
7
8
9
10
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,可以直接使用。


空集合。如果您使用的是C,那么假设最大化系统资源并不重要。虽然效率较低,但返回空集合对于所涉及的程序员更方便(原因如上所述)。


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


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

原因是方便了调用者的执行、一致的合同和更容易的执行。

如果方法返回空值以指示空结果,则调用方除了枚举之外还必须实现空值检查适配器。然后,这些代码在不同的调用者中被复制,所以为什么不将这个适配器放在方法中,以便可以重用它呢?

IEnumerable的有效用法NULL可能表示缺少结果或操作失败,但在这种情况下,应考虑其他技术,例如引发异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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());
        }
    }
}


I call it my billion-dollar mistake…At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
– Tony Hoare, inventor of ALGOL W.

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


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

(这相当于单元测试负担。除了空的集合返回案例外,还需要为空的返回案例编写一个测试。)