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


当前回答

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

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

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

其他回答

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

Suppose in, say, a system for a hospital, we have a function that is supposed to return a list of all previous hospitalizations for the past 5 years. If the customer has not been in the hospital, it makes good sense to return an empty list. But what if the customer left that part of the admittance form blank? We need a different value to distinguish "empty list" from "no answer" or "don't know". We could throw an exception, but it's not necessarily an error condition, and it doesn't necessarily drive us out of the normal program flow.

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


回复桑德斯:

Yes, I'm assuming that there's a difference between "Person didn't answer the question" and "The answer was zero." That was the point of the last paragraph of my answer. Many programs are unable to distinguish "don't know" from blank or zero, which seems to me a potentially serious flaw. For example, I was shopping for a house a year or so ago. I went to a real estate web site and there were many houses listed with an asking price of $0. Sounded pretty good to me: They're giving these houses away for free! But I'm sure the sad reality was that they just hadn't entered the price. In that case you may say, "Well, OBVIOUSLY zero means they didn't enter the price -- nobody's going to give a house away for free." But the site also listed the average asking and selling prices of houses in various towns. I can't help but wonder if the average didn't include the zeros, thus giving an incorrectly low average for some places. i.e. what is the average of $100,000; $120,000; and "don't know"? Technically the answer is "don't know". What we probably really want to see is $110,000. But what we'll probably get is $73,333, which would be completely wrong. Also, what if we had this problem on a site where users can order on-line? (Unlikely for real estate, but I'm sure you've seen it done for many other products.) Would we really want "price not specified yet" to be interpreted as "free"?

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,所以它对调用者没有更多的负担,它更少。

I don't see how it creates a DRY issue. It's not like we have to execute the call twice. If we always wanted to do the same thing when the list does not exist, maybe we could push handling down to the get-list function rather than having the caller do it, and so putting the code in the caller would be a DRY violation. But we almost surely don't want to always do the same thing. In functions where we must have the list to process, a missing list is an error that might well halt processing. But on an edit screen, we surely don't want to halt processing if they haven't entered data yet: we want to let them enter data. So handling "no list" must be done at the caller level one way or another. And whether we do that with a null return or a separate function makes no difference to the bigger principle.

Sure, if the caller doesn't check for null, the program could fail with a null-pointer exception. But if there's a separate "got any" function and the caller doesn't call that function but blindly calls the "get list" function, then what happens? If it throws an exception or otherwise fails, well, that's pretty much the same as what would happen if it returned null and didn't check for it. If it returns an empty list, that's just wrong. You're failing to distinguish between "I have a list with zero elements" and "I don't have a list". It's like returning zero for the price when the user didn't enter any price: it's just wrong.

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

A function that returns null is not a surprise if the programmer is familiar with the concept of null meaning "don't have a value", which I think any competent programmer should have heard of, whether he thinks it's a good idea or not. I think having a separate function is more of a "surprise" problem. If a programmer is unfamiliar with the API, when he runs a test with no data he'll quickly discover that sometimes he gets back a null. But how would he discover the existence of another function unless it occurred to him that there might be such a function and he checks the documentation, and the documentation is complete and comprehensible? I would much rather have one function that always gives me a meaningful response, rather than two functions that I have to know and remember to call both.

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

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.

See here for an elaborate shit storm about null in general. I do not agree with the statement that undefined is another null, but it is still worth reading. And it explains, why you should avoid null at all and not just in the case you have asked. The essence is, that null is in any language a special case. You have to think about null as an exception. undefined is different in that way, that code dealing with undefined behavior is in most cases just a bug. C and most other languages have also undefined behavior but most of them have no identifier for that in the language.

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

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

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

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

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

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