这是一个普遍的问题(但我使用的是 C#),最好的方法是什么(最佳实践),对于将集合作为返回类型的方法,您是返回 null 还是空集合?
空集合。总是。
这很糟糕:
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>()
可以被视为比返回(例如,新的空集合或数组)更有效。
从 Framework Design Guidelines 2nd Edition(第 256 页):
不要从集合属性或返回集合的方法返回空值。而是返回一个空集合或一个空数组。
这是另一篇关于不返回空值的好处的有趣文章(我试图在 Brad Abram 的博客上找到一些东西,他链接到了这篇文章)。
编辑- Eric Lippert 现在对原始问题发表了评论,我也想link to his excellent article。
取决于您的合同和具体情况。通常最好返回空集合,但有时(很少):
null 可能意味着更具体的东西;
您的 API(合同)可能会强制您返回 null。
一些具体的例子:
一个 UI 组件(来自您无法控制的库),如果传递一个空集合,则可能呈现一个空表,或者如果传递 null,则根本没有表。
在 Object-to-XML (JSON/whatever) 中,null 表示元素丢失,而空集合将呈现冗余(并且可能不正确)
您正在使用或实现一个明确声明应该返回/传递 null 的 API
还有一点尚未提及。考虑以下代码:
public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}
调用此方法时,C# 语言将返回一个空枚举器。因此,为了与语言设计(以及程序员的期望)保持一致,应该返回一个空集合。
Empty 对消费者更友好。
有一个明确的方法可以组成一个空的可枚举:
Enumerable.Empty<Element>()
在我看来,您应该返回在上下文中语义正确的值,无论它可能是什么。一条“总是返回一个空集合”的规则对我来说似乎有点简单。
假设在医院的系统中,我们有一个函数应该返回过去 5 年所有以前住院的列表。如果客户没有住院,返回一个空列表是很有意义的。但是,如果客户将准入表格的那部分留空怎么办?我们需要一个不同的值来区分“空列表”与“没有答案”或“不知道”。我们可以抛出异常,但这不一定是错误条件,也不一定会让我们脱离正常的程序流程。
我经常对无法区分零和无答案的系统感到沮丧。我有很多次系统要求我输入一些数字,我输入零,然后我收到一条错误消息,告诉我必须在此字段中输入一个值。我刚刚做了:我输入了零!但它不会接受零,因为它无法区分它和没有答案。
回复桑德斯:
是的,我假设“人没有回答问题”和“答案为零”之间存在差异。这就是我回答的最后一段的重点。许多程序无法将“不知道”与空白或零区分开来,这在我看来是一个潜在的严重缺陷。例如,大约一年前我正在买房子。我去了一个房地产网站,上面列出了许多要价为 0 美元的房子。对我来说听起来不错:他们免费赠送这些房子!但我确信可悲的现实是他们只是没有输入价格。在这种情况下,你可能会说,“嗯,显然为零意味着他们没有输入价格——没有人会免费赠送房子。”但该网站还列出了各个城镇房屋的平均要价和售价。我不禁想知道平均值是否不包括零,从而在某些地方给出了错误的低平均值。即 100,000 美元的平均值是多少; 120,000 美元;和“不知道”?从技术上讲,答案是“不知道”。我们可能真正想看到的是 110,000 美元。但我们可能会得到 73,333 美元,这是完全错误的。另外,如果我们在用户可以在线订购的网站上遇到这个问题怎么办? (不太可能适用于房地产,但我相信您已经看到许多其他产品都这样做了。)我们真的希望“尚未指定价格”被解释为“免费”吗?
RE 有两个独立的功能,一个“有吗?”和“如果是,那是什么?”是的,你当然可以这样做,但你为什么要这样做?现在调用程序必须进行两次调用而不是一次。如果程序员没有调用“any”会发生什么?并直奔“这是什么?” ?该程序会返回一个误导性的零吗?抛出异常?返回一个未定义的值?它会创建更多代码、更多工作和更多潜在错误。
我看到的唯一好处是它使您能够遵守任意规则。这条规则有什么好处,值得费心去遵守它吗?如果不是,为什么要打扰?
回复果酱蛋糕:
考虑一下实际代码的样子。我知道问题说的是 C#,但如果我写 Java,请原谅。我的C#不是很犀利,原理是一样的。
使用空返回:
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 的一两行代码,所以它对调用者来说不是更多的负担,而是更少。
我看不出它是如何产生 DRY 问题的。这不像我们必须执行两次调用。如果我们总是想在列表不存在时做同样的事情,也许我们可以将处理下推到 get-list 函数而不是让调用者来做,因此将代码放在调用者中将违反 DRY。但我们几乎可以肯定不想总是做同样的事情。在我们必须处理列表的函数中,缺少列表是一个很可能会停止处理的错误。但是在编辑屏幕上,如果他们还没有输入数据,我们肯定不想停止处理:我们想让他们输入数据。因此,必须以一种或另一种方式在调用者级别处理“无列表”。并且我们是否使用 null 返回或单独的函数来执行此操作对更大的原则没有影响。
当然,如果调用者不检查空值,程序可能会因空指针异常而失败。但是,如果有一个单独的“获取任何”函数并且调用者没有调用该函数而是盲目地调用“获取列表”函数,那么会发生什么?如果它抛出异常或以其他方式失败,那么这与如果它返回 null 并且没有检查它所发生的情况几乎相同。如果它返回一个空列表,那就错了。您无法区分“我有一个零元素的列表”和“我没有一个列表”。这就像当用户没有输入任何价格时,价格返回零:这是错误的。
我看不出将附加属性附加到集合有什么帮助。调用者仍然需要检查它。这比检查 null 有什么好处?同样,可能发生的最糟糕的事情是程序员忘记检查它,并给出错误的结果。
如果程序员熟悉 null 意味着“没有值”的概念,那么返回 null 的函数就不足为奇了,我认为任何有能力的程序员都应该听说过,无论他是否认为这是一个好主意。我认为拥有一个单独的功能更像是一个“惊喜”问题。如果程序员不熟悉 API,当他在没有数据的情况下运行测试时,他会很快发现有时他会返回 null。但是他怎么会发现另一个函数的存在,除非他想到可能存在这样的函数并且他检查了文档,并且文档是完整且易于理解的?我宁愿有一个总是给我一个有意义的响应的函数,而不是我必须知道并记住调用两个函数的两个函数。
如果一个空集合在语义上有意义,那是我更喜欢返回的。为 GetMessagesInMyInbox()
返回一个空集合表示“您的收件箱中确实没有任何消息”,而返回 null
可能有助于表示没有足够的数据来说明可能返回的列表应该是什么样子.
null
值肯定看起来不合理,我在考虑更笼统的术语。异常也可以很好地传达出现问题的事实,但是如果完全可以预期所指的“数据不足”,那么抛出异常将是糟糕的设计。我宁愿考虑一种情况,这种情况是完全有可能的,并且该方法有时无法计算响应完全没有错误。
返回 null 可能更有效,因为没有创建新对象。但是,它通常还需要 null
检查(或异常处理)。
从语义上讲,null
和空列表的含义不同。差异是微妙的,在特定情况下,一种选择可能比另一种更好。
无论您的选择如何,都要记录下来以避免混淆。
视情况而定。如果是特殊情况,则返回null。如果函数恰好返回一个空集合,那么显然返回是可以的。但是,由于参数无效或其他原因,将空集合作为特殊情况返回不是一个好主意,因为它掩盖了特殊情况条件。
实际上,在这种情况下,我通常更喜欢抛出异常以确保它真的不被忽略:)
说它使代码更健壮(通过返回一个空集合)因为它们不必处理 null 条件是不好的,因为它只是掩盖了应该由调用代码处理的问题。
我认为 null
与空集合不同,您应该选择哪个最能代表您要返回的内容。在大多数情况下,null
什么都不是(SQL 除外)。一个空的集合是一些东西,尽管是一个空的东西。
如果您必须选择其中一个,我会说您应该倾向于一个空集合而不是空集合。但有时空集合与空值不同。
始终支持您的客户(使用您的 api):
返回“null”通常会导致客户端无法正确处理 null 检查,这会在运行时导致 NullPointerException。我见过这样一个缺失的空检查强制优先生产问题的情况(客户端使用 foreach(...) 空值)。在测试过程中没有出现问题,因为操作的数据略有不同。
大约一周前,我们在工作的开发团队中进行了这样的讨论,我们几乎一致认为是空集合。一个人想返回 null 的原因与上面 Mike 指定的相同。
空集合。如果您使用的是 C#,则假设最大化系统资源不是必需的。虽然效率较低,但返回 Empty Collection 对于所涉及的程序员来说更方便(出于上述原因)。
我想在这里用合适的例子来解释。
在这里考虑一个案例..
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
我可以轻松地使用 ListCustomerAccount
和 FindAll
来代替。,
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 表示结果为空,则调用者必须实现除枚举之外的 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());
}
}
}
我称其为我的十亿美元错误……当时,我正在为面向对象语言的引用设计第一个综合类型系统。我的目标是确保所有引用的使用都应该是绝对安全的,并由编译器自动执行检查。但我无法抗拒加入空引用的诱惑,仅仅是因为它很容易实现。这导致了无数的错误、漏洞和系统崩溃,在过去的四十年中可能造成了十亿美元的痛苦和损失。 ——托尼·霍尔,ALGOL W 的发明者。
请参阅 here,了解关于 null
的详细说明。我不同意 undefined
是另一个 null
的说法,但它仍然值得一读。它解释了为什么你应该完全避免 null
而不仅仅是在你问的情况下。本质是,null
在任何语言中都是一种特殊情况。您必须将 null
视为一个例外。 undefined
在这方面有所不同,处理未定义行为的代码在大多数情况下只是一个错误。 C 和大多数其他语言也有未定义的行为,但它们中的大多数在语言中没有标识符。
从管理复杂性(软件工程的主要目标)的角度来看,我们希望避免向 API 的客户端传播不必要的圈复杂性。向客户端返回 null 就像向他们返回另一个代码分支的圈复杂度成本。
(这对应于单元测试负担。除了空集合返回案例之外,您还需要为 null 返回案例编写测试。)
Go 似乎是一种语言,其中 nil
优于空数组。
https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices
声明空切片时,首选 var t []string 而非 t := []string{}。前者声明一个 nil 切片值,而后者是非 nil 但长度为零。它们在功能上是等效的——它们的 len 和 cap 都为零——但 nil 切片是首选样式。
,
可能会返回空集合;如果没有带有 id="foo" 的
,则返回 null 会更好(除非您想处理这种情况并出现异常)
IEnumerable
还是ICollection
并不重要。无论如何,如果您选择ICollection
类型的东西,它们也会返回null
...我希望它们返回一个空集合,但我遇到它们返回null
,所以我想我只是在这里提一下.我会说可枚举集合的默认值为空而不是空。我不知道这是一个如此敏感的话题。