[c#] 返回null还是空集合更好?


Answers

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

不要从集合属性或返回集合的方法返回空值。 改为返回空集合或空数组。

这里有另一篇有趣的文章,介绍了不返回空值的好处(我试图在布拉德阿布拉姆的博客上找到一些东西,并且他与文章相关)。

编辑 -正如Eric Lippert现在对最初的问题所述,我还想blogs.msdn.com/ericlippert/archive/2009/05/14/…

Question

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




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

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

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




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

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

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

回复桑德斯:

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

RE有两个独立的功能,“有没有?” 和“如果是,那是什么?” 是的,你当然可以做到这一点,但你为什么要? 现在呼叫程序必须拨打两个电话而不是一个电话。 如果程序员不能称之为“任何”,会发生什么? 并直接进入“这是什么?” ? 程序是否会返回一个错误的前导零? 抛出异常? 返回一个未定义的值? 它会创建更多的代码,更多的工作和更多潜在的错误。

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

回复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 ...
}

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

我不明白它是如何造成干的问题。 这不像我们必须执行两次呼叫。 如果我们总是希望在列表不存在时做同样的事情,也许我们可以将处理降低到get-list函数,而不是让调用者这样做,因此将代码放在调用者中会是DRY违规。 但是我们几乎肯定不想总是做同样的事情。 在我们必须处理列表的函数中,缺少的列表是一个可能会导致处理停止的错误。 但是在编辑屏幕上,如果他们还没有输入数据,我们肯定不想停止处理:我们想让他们输入数据。 因此,处理“没有列表”必须以这样或那样的方式在呼叫者级别完成。 无论我们是用空回报还是单独的函数来做这件事,对于更大的原则都没有什么不同。

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

我没有看到如何将附加属性附加到集合上有所帮助。 来电者仍然必须检查它。 如何比检查null更好? 再次,可能发生的最糟糕的事情是程序员忘记检查它,并给出不正确的结果。

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




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

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

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




总是倾向于你的客户(使用你的api):

返回'null'通常会造成客户端无法正确处理空检查的问题,这会在运行时导致NullPointerException。 我曾见过这种缺少空检查的情况,强制产生一个优先级生产问题(一个客户端在空值上使用foreach(...))。 在测试过程中,问题没有发生,因为操作的数据稍有不同。




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

这样做的原因是实现呼叫者方便,合同一致,并且更容易实施。

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

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

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

namespace .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://.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());
        }
    }
}



我们在开发团队在一周前的某个工作日之前进行了讨论,并且我们几乎一致地进行了空收集。 有一个人想要返回null,这与Mike上面指出的原因相同。




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

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

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




Related