我应该在遍历它们的方法中接受空集合吗?


40

我有一个方法,其中所有逻辑都在foreach循环内执行,该循环遍历该方法的参数:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

在这种情况下,发送一个空集合会导致一个空集合,但是我想知道这是否不明智。

我的逻辑是,如果有人正在调用此方法,那么他们打算传入数据,并且只会在错误的情况下将空集合传递给我的方法。

我应该捕获此行为并抛出异常,还是返回空集合的最佳实践?


43
您确定“如果有人正在调用此方法,那么他们打算传递数据”的假设是正确的吗?也许他们只是通过手头的东西来处理结果。
SpaceTrucker 2014年

7
顺便说一句:您的“转换”方法通常称为“映射”,尽管在.NET(和SQL,.NET从中获取名称)中,通常将其称为“选择”。C ++称之为“转换”,但这不是一个可能会识别的通用名称。
约尔格W¯¯米塔格

25
如果集合是null空的,我会扔,但如果它是空的,我不会扔。
CodesInChaos

21
@NickUdell-我一直在代码中传递空集合。对于在无法继续进行任何操作之前无法向集合中添加任何内容的情况,代码的行为比编写特殊的分支逻辑要容易得多。
theMayer

7
如果您检查并在集合为空的情况下引发错误,那么您将迫使调用者也检查而不将空集合传递给您的方法。验证应该在系统边界上(并且仅在系统边界上)进行,如果空集合困扰您,则您应该早已对它们进行检查,直到它到达任何实用程序方法为止。现在,您有两个冗余检查。
Lie Ryan

Answers:


175

实用程序方法不应抛出空集合。您的API客户端会讨厌您。

集合可以为空;从概念上讲,“一定不能为空的集合”是一件更加困难的事情。

转换一个空集合有一个明显的结果:空集合。(您甚至可以通过返回参数本身来节省一些垃圾。)

在许多情况下,模块会维护可能已经填充或可能尚未填充的东西的列表。在每次调用之前都必须检查是否为空,这transform很烦人,并且有可能将简单,优雅的算法变成丑陋的混乱。

效用方法应始终努力使其输入自由,并在输出中保持保守。

由于所有这些原因,看在上帝的份上,正确处理空集合。没有什么比帮助程序模块更令人生气的了,该模块认为它比您更了解自己想要的东西。


14
+1-(如果可以的话,还可以)。我什至可以进一步整合null成空集合。具有隐式限制的功能很痛苦。
Telastyn 2014年

6
“不,您不应该” ...不应该接受它们,也不应该抛出异常?
Ben Aaronson 2014年

16
@Andy-那些不是实用程序方法。它们将是商业方法或一些类似的具体行为。
Telastyn 2014年

38
我不认为回收空的收藏品是个好主意。您只节省了一点内存;并可能导致调用方出现意外行为。稍作示例:Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); 如果Populate1将集合留空,并在第一次TransformNodes调用时将其返回,则output1和input将是同一集合,并且如果调用Populaten2确实将节点放入集合中,则您将以第二个结束输出2中的一组输入。
Dan Neely 2014年

6
@Andy即便如此,如果参数永远不能为空-如果没有要处理的数据是一个错误-那么我认为在整理该参数时,调用方有责任检查该参数。它不仅使这种方法更加优雅,而且还意味着人们可以生成更好的错误消息(更好的上下文)并以快速失败的方式提出它。对于下游类来说,验证调用者的不变量是没有意义的……
Andrzej Doyle 2014年

26

我看到两个重要的问题,这些问题决定了答案:

  1. 传递空集合(包括null)时,您的函数能否返回有意义且合乎逻辑的内容?
  2. 该应用程序/库/团队中的一般编程风格是什么?(具体地说,您是FP吗?)

1.有意义的回报

本质上,如果您可以返回有意义的内容,则不要抛出异常。让调用者处理结果。所以如果你的功能...

  • 计算集合中的元素数,返回0。这很容易。
  • 搜索与特定条件匹配的元素,返回一个空集合。请不要扔任何东西。调用方可能有大量的集合,其中有些是空的,有些不是空的。调用方需要任何集合中的任何匹配元素。例外只会使呼叫者的生活更加艰难。
  • 正在寻找列表中最大/最小/最适合的标准。哎呀。根据样式问题,您可以在此处引发异常,也可以返回null。我讨厌null(很多FP人员),但是在这里它可能更有意义,它可以让您为自己代码中的意外错误保留异常。如果呼叫者不检查null,无论如何都会产生一个相当明确的异常。但是,您将其留给他们。
  • 询问集合中的第n个项目或前/后n个项目。这是发生异常的最佳情况,也是最不可能给调用者造成混乱和困难的一种情况。如果您和您的团队习惯于出于上述原因给出的所有原因进行检查,那么仍然可以将null设置为,但这是引发DudeYou Know YouShouldCheckTheSizeFirst异常的最有效的情况。如果您的样式更具实用性,请为null或继续阅读我的样式答案。

通常,我的FP偏向告诉我“返回有意义的东西”,在这种情况下,null可能具有有效的含义。

2.风格

您的通用代码(或项目代码或团队代码)是否偏爱功能样式?如果否,则将预期并处理异常。如果是,则考虑返回Option Type。对于选项类型,您将返回有意义的答案或None / Nothing。在上面的第三个示例中,没有什么是FP风格的好答案。该函数返回Option类型的事实可能无法明确地向调用者发出信号,而不是有意义的答案,因此应准备好调用者进行处理。我觉得这为呼叫者提供了更多选择(如果您会原谅双关语的话)。

F#是所有很酷的.Net孩子做这样的事情,但C# 支持这种风格。

tl; dr

在您自己的代码路径中保留意外错误的异常,而不是别人的输入完全可预见(合法)。


“最适合”等:您可能有一种方法,可以在符合某些条件的集合中找到最适合的对象。对于非空集合,该方法将存在相同的问题,因为没有任何一个符合条件,因此无需担心空选择。
gnasher729 2014年

1
不,这就是为什么我说“最合适”而不是“匹配”;该短语表示最接近的近似值,而不是精确匹配。最大/最小选项应该是一个线索。如果只要求“最合适”,则任何具有1个或更多成员的集合都应该能够返回一个成员。如果只有一名成员,那是最合适的。
itsbruce 2014年

1
在大多数情况下,返回“ null”会更糟,然后引发异常。但是,返回一个空集合是有意义的。
伊恩

1
如果您必须退货(最小/最大/头/尾),则不需要。至于null,正如我所说,我讨厌它(并且更喜欢没有它的语言),但是如果语言确实有它,则您必须能够处理它并编写将其分发出去的代码。根据本地编码约定,这可能是适当的。
itsbruce 2014年

您的示例都与空输入无关。它们只是答案可能为“无”的特殊情况。这反过来又应该回答OP关于如何“处理”空集合的问题。
djechlin

18

与以往一样,这取决于。

集合为空是否重要
大多数收集处理代码可能会说“否”。集合中可以包含任意数量的项目,包括零。

现在,如果您有某种类型的集合,其中“没有”项是“无效的”,那么这是一项新要求,您必须决定如何处理。

从数据库世界借用一些测试逻辑:测试项,一项两项。这可以满足最重要的情况(清除任何格式不正确的内部或笛卡尔联接条件)。


并且,如果在集合中没有任何项目是无效的,那么理想情况下,该参数就不是一个java.util.Collection,而是一个自定义com.foo.util.NonEmptyCollection类,该类可以一致地保留此不变式,并防止您陷入无效状态。
Andrzej Doyle

3
这个问题被标记为[c#]
尼克·乌德尔2014年

@NickUdell C#不支持面向对象的编程或嵌套名称空间的概念吗?瓷砖。
约翰·德沃夏克

2
是的 我的评论是澄清的,因为Andrzej一定很困惑(为什么他们为什么还要努力为这个问题未引用的语言指定名称空间?)。
尼克·乌德尔2014年

-1比“几乎肯定是”更强调“取决于”。别开玩笑-传达错误的内容确实会造成伤害。
djechlin

11

作为一个好的设计,要尽可能多地接受您输入的变化。异常应在(接受输入呈现或在处理意外的错误发生)被抛出,程序无法预测的方式继续作为一个结果。

在这种情况下,应该期望将显示一个空集合,并且您的代码需要处理它(它已经这样做了)。如果您的代码在此处引发异常,那么这将违反所有的好处。这类似于在数学中将0乘以0。它是多余的,但绝对必须按其工作方式进行。

现在,转到null集合参数。在这种情况下,空集合是编程错误:程序员忘记分配变量。在这种情况下,可能会引发异常,因为您无法有意义地将其处理为输出,并且尝试这样做会引入意外的行为。这类似于数学中的零除-完全没有意义。


完全笼统地说,您的大胆声明是极坏的建议。我认为这是对的,但是您需要解释这种情况的特殊之处。(一般来说,这是个坏建议,因为它会导致无声的失败。)
djechlin 2014年

@AAA-显然我们不是在这里写有关如何设计软件的书,但是,如果您真的了解我在说什么,我认为这根本不是一个坏建议。我的观点是,不良输入会产生歧义或任意输出,您需要抛出异常。如果输出是完全可预测的(如此处的情况),则抛出异常将是任意的。关键是永远不要在程序中做出任意决定,因为这是不可预测行为的来源。
theMayer 2014年

但这是您的第一句话,也是唯一突出显示的句子...没人知道您在说什么。(我在这里并不想太激进,我们要做的其中一件事就是学会写作和交流,我认为关键点上的精确度下降是有害的。)
djechlin

10

当您单独查看功能时,很难找到正确的解决方案。将您的功能视为更大问题的一部分。该示例的一种可能解决方案如下所示(在Scala中):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

首先,将字符串按非数字拆分,过滤出空字符串,将字符串转换为整数,然后过滤以仅保留四位数。您的功能可能map (_.toInt)在管道中。

该代码非常简单,因为管道中的每个阶段都只处理一个空字符串或一个空集合。如果在开头放置一个空字符串,则在末尾会得到一个空列表。您不必null在每次通话后都停下来检查是否有异常。

当然,这是假定一个空的输出列表没有多个含义。如果需要区分由空输入引起的空输出和由转换本身引起的空输出,那将完全改变事情。


+1是非常有效的示例(其他没有好的答案)。
djechlin

2

这个问题实际上是关于异常的。如果您以这种方式看待它,而忽略空集合作为实现细节,那么答案很简单:

1)方法无法继续执行时应抛出异常:要么无法执行指定的任务,要么返回适当的值。

2)尽管失败,方法仍然能够继续执行,则该方法应捕获异常。

因此,您的辅助方法不应是“有帮助的”并且抛出异常,除非无法使用空集合来完成它的工作。让调用者确定结果是否可以处理。

无论返回空集合还是null,都会有些困难,但难度不大:如果可能,应避免可空集合。可为空的集合的目的是指示(如SQL中一样)您没有该信息-例如,如果您不知道某人是否有子项,则子集合可能为null。知道他们不知道。但是,如果由于某些原因这很重要,则可能值得添加一个额外的变量来对其进行跟踪。


1

该方法名为TransformNodes。在输入空集合的情况下,取回空集合是自然而直观的,并且具有完善的数学意义。

如果该方法被命名Max并设计为返回最大元素,那么自然会抛出NoSuchElementException一个空集合,因为最大的无意义在数学上是没有意义的。

如果该方法的名称被JoinSqlColumnNames设计为返回一个字符串,该字符串中的元素由逗号连接以用于SQL查询,则抛出IllegalArgumentException一个空集合是有意义的,因为如果调用者使用了该方法,最终还是会遇到SQL错误直接在SQL查询中查询字符串,而无需进一步检查,他实际上应该检查空集合,而不是检查返回的空字符串。


Max一无所有通常是负无穷大。
djechlin

0

让我们退后一步,使用另一个示例,该示例计算值数组的算术平均值。

如果输入数组为空(或null),则可以合理地满足调用者的请求吗?不,您有什么选择?好吧,您可以:

  • 存在/返回/抛出错误。使用您的代码库惯例来处理此类错误。
  • 文档将返回诸如零的值
  • 文档,将返回指定的无效值(例如,NaN)
  • 记录将返回魔术值(例如,类型的最小值或最大值或一些希望指示的值)
  • 声明结果未指定
  • 声明动作未定义
  • 等等

我说如果他们给您无效的输入并且请求无法完成,请给他们错误。我的意思是从第一天开始就出现了严重错误,因此他们了解您的程序要求。毕竟,您的功能无法响应。如果操作可能失败(例如,复制文件),那么您的API应该给他们一个可以处理的错误。

这样可以定义您的库如何处理格式错误的请求以及可能失败的请求。

对于您的代码来说,在处理这些错误类别方面保持一致非常重要。

下一个类别是决定您的图书馆如何处理废话请求。回到与您类似的示例-让我们使用一个函数来确定文件是否存在于路径:bool FileExistsAtPath(String)。如果客户端传递一个空字符串,您如何处理这种情况?传递给空数组或空数组如何void SaveDocuments(Array<Document>)?确定您的库/代码库,并保持一致。我碰巧考虑了这些案例错误,并通过将它们标记为错误(通过断言)来禁止客户端发出无意义的请求。有些人会坚决抵制该想法/行动。我发现此错误检测非常有帮助。这对于在程序中查找问题非常有用-定位到有问题的程序的适当位置。程序更加清晰和正确(考虑代码库的演变),并且不会在无能为力的函数中燃烧循环。这样,代码的大小就会变得更小/更干净,并且检查通常会推到可能引入问题的位置。


6
对于最后一个示例,确定什么废话不是该功能的工作。因此,空字符串应返回false。确实,这就是File.Exists采取的确切方法。
2014年

@ rmayer06这是它的工作。引用的函数允许空字符串,空字符串,检查字符串中的无效字符,执行字符串截断,可能需要进行文件系统调用,可能需要查询环境等。那些经常执行冗余的“便利”有成本高昂,而且它们肯定会模糊正确性(IMO)的界限。我见过很多if (!FileExists(path)) { ...create it!!!... }错误-正确性的界限没有模糊,很多错误会在提交之前被发现。
贾斯汀2014年

2
如果函数的名称为File.ThrowExceptionIfPathIsInvalid,但如果在他们的头脑中谁会调用该函数,我会同意你的看法。
2014年

仅仅由于该函数的定义方式,平均0项将已经返回NaN或抛出被零除的异常。一个名称可能不存在的文件,可能不存在,或者已经触发错误。没有迫切的理由要专门处理这些案件。
cHao 2014年

甚至可以说,在读取或写入文件之前检查文件是否存在的整个概念还是有缺陷的。(存在一个固有的竞争条件。)最好继续尝试打开文件进行读取,或者如果要进行写入则使用仅创建设置。如果文件名无效或不存在(或者在写入时确实存在),它将已经失败。
cHao 2014年

-3

根据经验,标准函数应该能够接受最广泛的输入列表并给出反馈。在许多示例中,程序员以设计人员未计划的方式使用函数,因此我相信函数应该不仅能够接受空集合,而且还能够接受各种输入类型,并且能够优雅地返回反馈,即使它是对输入执行的任何操作的错误对象……


设计人员认为一个集合将始终被传递并直接迭代该集合是绝对错误的,必须进行检查以确保接收到的参数符合预期的数据类型...
Clement Mark-Aaba

3
这里的“输入类型范围广泛”看起来无关紧要。问题被标记为c#(一种强类型语言)。也就是说,编译器保证输入是一个集合
gnat

请问Google是“经验法则”,我的回答是普遍警告,作为程序员,您为无法预料或错误的事件腾出空间,其中之一可能会错误地传递函数参数...
Clement Mark-Aaba 2014年

1
这要么是错误的,要么是重言式的。writePaycheckToEmployee不应接受负数作为输入...但是,如果“ feedback”表示“接下来会发生任何事情”,则是的,每个函数都将执行下一步要做的事情。
djechlin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.