期望Any()* not *引发null引用异常是否不合理?


41

当你创建一个扩展方法就可以了,当然,调用它null。但是,不像一个实例方法调用,调用它在空不具有NullReferenceException- >您必须检查和手动把它。

为了实施Linq扩展方法,Any()Microsoft决定他们应该抛出一个ArgumentNullExceptionhttps://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/AnyAll.cs)。

我不得不写信 if( myCollection != null && myCollection.Any() )

作为此代码的客户端,我期望eg ((int[])null).Any()应该返回false吗?


41
在C#6中,您可以简化检查是否为(myCollection?.Any()== true)
18年

5
即使在F#中,除了与其他.NET语言的互操作性之外,它实际上并不使用null,它也会null |> Seq.isEmpty引发throws System.ArgumentNullException: Value cannot be null。期望似乎是您不会为预期存在的内容传递未定义的值,因此当您具有null时,这仍然是一个例外。如果是初始化问题,我将从一个空序列开始,而不是一个null
亚伦·埃希巴赫

21
这就是为什么null在处理集合时您不应该返回而是在这种情况下使用空集合的原因。
Bakuriu

31
那么为什么它首先是null?您不希望将null计为空,因为它不为空,而是完全不同的东西。也许您希望在您的情况下将其计为空,但是在语言中添加此类内容会导致错误。
user253751 '18

22
我应该指出,每一个 LINQ方法都会抛出一个空参数。Any是一致的。
塞巴斯蒂安·雷德尔

Answers:


157

我有一个装着五个土豆的袋子。.Any()袋子里有土豆吗?

是的,”你说。<= true

我把所有的土豆拿出来吃。.Any()袋子里有土豆吗?

,”你说。<= false

我完全用火焚烧了袋子。.Any()现在袋子里有土豆吗?

没有包。”<= ArgumentNullException


3
但是,空虚地,袋子里没有土豆吗?错误代表错误……(我不同意,只是第二个观点)。
D. Ben Knoble

6
实际上,有人从未知来源给您一个密闭的盒子。盒子里的袋子里有土豆吗?我只对两种情况感兴趣-A)盒子里有一个装有土豆的袋子,或者B)盒子里没有土豆和/或没有袋子。从语义上讲,是的,null和empty之间是有区别的,但是我有99%的时间不在乎。
戴夫·蒂本

6
你吃了五个生土豆?立即就医。
奥利(Oly)

3
@Oly Dan在饭前用大火煮熟,然后用大火将袋子焚化。
伊斯梅尔·米格尔

3
@ D.BenKnoble:在没有看到我的汽车后备箱的情况下,您愿意证明我的汽车后备箱中没有尸体吗?没有?检查中继与不查找任何东西,或者不检查中继之间有什么区别?由于两种情况下您看到的尸体数量完全相同,您现在不能作证吗?...就是这样。如果您不能一开始就真正确认,就不能担保。您假定两者是相等的,但这不是给定的。在某些情况下,两者之间可能会有重要区别。
扁平化

52

首先,似乎源代码将抛出ArgumentNullException,而不是NullReferenceException

话虽如此,在许多情况下,您已经知道集合不为null,因为仅从知道该集合已存在的代码中调用此代码,因此您不必经常在其中进行null检查。但是,如果您不知道它的存在,那么在调用Any()它之前进行检查确实很有意义。

作为此代码的客户端,我期望eg ((int[])null).Any()应该返回false吗?

是。现在的问题是Any()答案是“此集合包含任何元素?” 如果这个集合不存在,那么问题本身就是荒谬的。它既不能包含也不可以不包含任何东西,因为它不存在。


18
@ChrisWohlert:您的直觉引起了我的兴趣。在语义上,您是否还认为不存在的人是空的人,其名字是空字符串且年龄为零,依此类推?而且您是否认为包含的集合null等同于包含空集合的集合(反之亦然)?
ruakh

17
@ChrisWohlert:显然一个集合可以包含空集合;但你似乎认为,{ null }{ {} }应该是等价的。我觉得那很迷人;我一点都没有关系。
ruakh

9
如果.Addnull收集某种方式为我们创造一个集吗?
马修(Matthew)

13
@ChrisWohlert“ A是一个空集。B是一个包含西瓜的集。是A空吗?是。B空吗?否。C空吗?

16
讽刺的是:在集合论中,不存在的集合并不是空集合。空集存在,并且不是任何不存在的集(实际上,它根本不是任何东西,因为它不存在)。
James_pic

22

空意味着缺少信息,而不是没有元素。

您可能会考虑更广泛地避免使用null-例如,使用内置的空可枚举之一来表示不包含元素而不是null的集合。

如果在某些情况下返回null,则可以更改该值以返回空集合。(否则,如果您发现库方法(不是您的方法)返回的null,那是不幸的,我将它们包装起来进行归一化。)

另请参阅 https://stackoverflow.com/questions/1191919/what-does-linq-return-when-the-results-are-empty


1
null并不意味着没有任何元素是使用明智的约定的问题。试想一下本地OLESTRINGS,实际上它的含义就是这样。
Deduplicator18

1
@Deduplicator,您是在谈论Microsoft的对象链接和嵌入性能吗?如果您想起来,OLE是90年代的产品!目前看来,许多现代语言(C#,Java,Swift)都提供了消除/消除null用法的工具。
埃里克·埃德特

2
@ErikEidt之所以这样做,部分是因为null这并不意味着“缺少信息”。null没有一个有意义的解释。相反,这取决于您如何对待它。null有时用于“缺少信息”,也用于“无元素”,也用于“发生错误”或“未初始化”或“信息冲突”或某些任意的临时性东西。
Derek Elkins '18

-1。这是开发人员的决定,而不是抽象理论的决定。null绝对经常用来表示“没有数据”。有时候这很有意义。其他时间没有。
jpmc26

13

除了空条件语法之外,还有另一种缓解此问题的技术:不要让变量永远存在null

考虑一个接受集合作为参数的函数。如果出于该功能的目的,null并且empty是等效的,则可以确保其null开头不包含:

public MyResult DoSomething(int a, IEnumerable<string> words)
{
    words = words ?? Enumerable.Empty<string>();

    if (!words.Any())
    {
        ...

通过其他方法获取集合时,您可以执行以下操作:

var words = GetWords() ?? Enumerable.Empty<string>();

(请注意,如果您可以控制GetWordsnull等于空集合的函数,则最好首先返回空集合。)

现在,您可以对集合执行您希望的任何操作。如果您需要执行许多操作,而这些操作在collection为时会失败,则这特别有用null,并且在通过循环或查询空可枚举获得相同结果的情况下,它将if完全消除条件。


12

作为此代码的客户端,我是否期望例如((int [])null).Any()应该返回false?

是的,只是因为您使用的是C#并且行为明确定义并记录在案。

如果您要创建自己的库,或者您使用的是具有不同异常文化的其他语言,那么期望false是更合理的。

我个人认为好像返回false是一种更安全的方法,可以使您的程序更强大,但至少值得商de。


15
这种“健壮”通常是一种错觉-如果由于错误或其他问题而导致生成数组的代码突然开始意外地生成NULL,那么您的“健壮”代码将隐藏问题。有时这是适当的行为,但不是安全的默认设置。
GoatInTheMachine

1
@GoatInTheMachine-我不同意它是一个安全的默认值,但是您是对的,它确实隐藏了问题,并且有时这种行为是不可取的。以我的经验,隐藏问题(或信任单元测试来捕获其他代码的问题)比抛出意外异常使应用程序崩溃更好。我的意思是真的-如果调用代码被破坏得足以发送空值,那么它们正确处理异常的机会是什么?
Telastyn

10
如果有问题发生了,无论是我的代码,其他同事还是客户的代码,我宁愿代码立即失败,人们知道它,而不是在不确定的时间内保持不确定状态。能够做到这一点当然有时是一种奢侈,并不总是恰当的,但这是我更喜欢的方法。
GoatInTheMachine

1
@GoatInTheMachine-我同意很多事情,但是收藏往往有所不同。将null视为一个空集合并不是一个非常不一致或令人惊讶的行为。
Telastyn

8
假设您有一个通过Web请求接收到的JSON文档填充的数组,并且在生产中,您的API的一个客户端突然出现问题,即它们通过部分无效的JSON发送,导致您将数组反序列化为NULL,您宁愿:在最坏的情况下,代码会在第一个错误请求进入时抛出NULL引用异常,您会在日志记录中看到该异常,然后立即告诉客户端修复其损坏的请求,或者您的代码说“哦,数组为空,什么都没有去做!” 并默默地向数据库管理员写了“采购篮没有任何物品”几周了?
GoatInTheMachine

10

如果重复的null检查使您烦恼,则可以创建自己的“ IsNullOrEmpty()”扩展方法,以该名称镜像String方法,并将null检查和.Any()调用都包装到一个调用中。

否则,由您的问题下的注释中的@ 17在26中提到的解决方案也比“标准”方法短,并且对于熟悉新的空条件语法的任何人都相当清楚。

if(myCollection?.Any() == true)

4
您也可以使用?? false代替== true。在我看来,这似乎更明确(更清晰),因为它处理的情况null,而实际上并没有那么冗长。加上==布尔值检查通常会触发我的堵嘴反射。=)
jpmc26 '18

2
@ jpmc26:我对C#不太了解。为什么还myCollection?.Any()不够?
埃里克·杜米尼尔

6
@EricDuminil由于空条件运算符的工作方式,myCollection?.Any()有效地返回了一个nullable-Boolean而不是普通的布尔值(即bool?代替bool)。没有bool的隐式转换?布尔值,在这个特定示例中我们需要布尔值(作为if条件的一部分进行测试)。因此,我们必须显式地与truenull运算符(??)进行比较或使用。
史蒂文·兰德斯'18

@ jpmc26?false比myCollection?.Any()== true更难读。不管您的堵嘴反射如何,== true都是您要隐式测试的内容。?? false只会增加额外的噪音,您仍然需要仔细评估如果null(null == true)将失败的情况,几乎不需要读者甚至也无需注意?.
Ryan The Leach

2
@RyanTheLeach我必须更加认真地考虑是否==会真正起作用。我已经知道当布尔值只能是true或时,凭直觉会发生什么false。我什至不用考虑。但是,将null其混为一谈,现在我必须确定这==是否正确。??相反,只是消除了这种null情况,将其还原为truefalse,您已经立即了解了。至于我的呕吐反射,当我第一次看到它时,我的直觉是直接删除它,因为它通常是无用的,您不想发生这种情况。
jpmc26

8

作为此代码的客户端,我是否期望例如((int [])null).Any()应该返回false?

如果您对期望感到好奇,则必须考虑意图。

null 意味着与 Enumerable.Empty<T>

Erik Eidt的回答所述null空集合之间的含义有所不同。

让我们首先看一下应该如何使用它们。

Microsoft架构师Krzysztof CwalinaBrad Abrams撰写的《框架设计指南:可重用.NET库的约定,惯用语和模式》第二版指出了以下最佳实践:

X请勿从集合属性或返回集合的方法中返回空值。而是返回一个空集合或一个空数组。

考虑调用一个最终从数据库中获取数据的方法:如果您收到一个空数组,或者Enumerable.Empty<T>这仅表示您的样本空间为空,即您的结果为一个空集null但是,在这种情况下接收将表示错误状态

按照与丹·威尔逊Dan Wilson)的马铃薯类比法相同的思路,即使数据是空集,也要提出有关您的数据的问题是有意义的。但它使少了很多道理,如果没有设置


1
它们是否具有不同的含义在很大程度上取决于上下文。通常,出于手头逻辑的目的,它们表示等效的概念。并非总是如此,但经常如此。
jpmc26

1
@ jpmc26:我认为这个答案的意思是说,通过为c#编码准则,它们的确具有不同的含义。如果需要,您总是可以对null和empty相同的代码进行修改。在某些情况下,无论它为null还是为空,您都可能会做同样的事情,但这并不意味着它们意味着同样的事情。
克里斯(Chris

@Chris我是说,设计该语言的人员的编码指南不能代替在您的要求,所用的库以及与您一起工作的其他开发人员的上下文中评估您的代码的替代方法。他们在上下文中永远不会等同的想法是天空中的理想主义。通常,它们甚至可能不等同于手头的数据,但等同于在您正在编写的某些特定函数中生成结果。在这种情况下,您需要同时支持这两种方法,但要确保它们产生相同的结果。
jpmc26

我可能对您所说的感到困惑,但是当null和empty等同于生成结果时,我不明白为什么您不只是使用split是null和为空检查,而不是想要同时执行两个操作的方法,但是不允许您在其他情况下区分它们……
克里斯

@Chris我指的是具有不同的上下文(通常对应于范围)。例如考虑一个功能。null和empty可能导致该函数的返回值相同,但这并不意味着null和empty在调用范围中具有完全相同的含义。
jpmc26

3

有很多答案解释了为什么null和空值是不同的,并且有足够多的意见试图解释为什么应该区别对待或不区别对待。但是你问:

作为此代码的客户端,我是否期望例如((int [])null).Any()应该返回false?

这是完全合理的期望。您和主张当前行为的其他人一样正确。我同意当前的实施理念,但驱动因素不仅仅基于上下文之外的考虑。

鉴于Any()基本上没有谓词,Count() > 0那么您对这段代码有何期待?

List<int> list = null;
if (list.Count > 0) {}

或通用:

List<int> list = null;
if (list.Foo()) {}

我想你期望NullReferenceException

  • Any()是扩展方法,它应该扩展对象平滑集成,然后引发异常是最令人惊讶的事情。
  • 并非每种.NET语言都支持扩展方法。
  • 您可以随时打电话Enumerable.Any(null),您一定会在那儿ArgumentNullException。这是相同的方法,必须与框架中的几乎所有其他内容保持一致。
  • 访问null对象是编程错误,框架不应将null强制为魔术值。如果您以这种方式使用它,那么您有责任处理它。
  • 它的自以为是,你认为一个方式,我想另一种方式。框架应尽可能不受质疑。
  • 如果您有特殊情况,那么您必须保持一致:您必须对所有其他扩展方法做出越来越多的自以为是的决定:如果Count()看起来很简单,那么Where()就不是。那Max()呢 它为EMPTY列表引发异常,是否也应该为一个列表引发异常null

库设计者在LINQ之前做的事情是引入显式方法时(例如)null是一个有效值String.IsNullOrEmpty()然后他们必须与现有设计理念相一致。就是说,即使编写起来很简单EmptyIfNull()EmptyOrNull()也可以使用两种方法。


1

吉姆应该把土豆放在每个袋子里。不然我要杀了他

吉姆有一个袋子,里面装着五个土豆。.Any()袋子里有土豆吗?

“是的。”你说。<=真

好吧,吉姆这次住了。

吉姆把所有的土豆拿出来吃了。袋子里有土豆吗?

“不,”你说。<=错误

是时候杀死吉姆了。

吉姆彻底将袋子焚化。袋子里有土豆吗?

“没有包。” <= ArgumentNullException

吉姆该死还是该死?好吧,我们没想到这一点,所以我需要裁决。让Jim摆脱这个错误还是没有?

您可以使用注释来表示您不会以这种方式忍受任何null的恶作剧。

public bool Any( [NotNull] List bag ) 

但是您的工具链必须支持它。这意味着您可能最终仍会写支票。


0

如果这让您如此困扰,我建议一种简单的扩展方法。

static public IEnumerable<T> NullToEmpty<T>(this IEnumerable<T> source)
{
    return (source == null) ? Enumerable.Empty<T>() : source;
}

现在您可以执行以下操作:

List<string> list = null;
var flag = list.NullToEmpty().Any( s => s == "Foo" );

...,标志将设置为false


2
更自然地将return声明写为:return source ?? Enumerable.Empty<T>();
Jeppe Stig Nielsen

这不能回答所问的问题。
肯尼斯·K

@KennethK。,但它确实可以解决提出的问题。
RQDQ

0

这是一个有关C#扩展方法及其设计理念的问题,因此我认为,回答此问题的最佳方法是引用MSDN的文档以扩展方法为目的

扩展方法使您可以将方法“添加”到现有类型,而无需创建新的派生类型,重新编译或修改原始类型。扩展方法是一种特殊的静态方法,但是它们的调用就像是扩展类型上的实例方法一样。对于用C#,F#和Visual Basic编写的客户端代码,在调用扩展方法和实际在类型中定义的方法之间没有明显的区别。

通常,我们建议您仅在必要时才少量实施扩展方法。只要有可能,必须扩展现有类型的客户端代码都应该通过创建从现有类型派生的新类型来进行扩展。有关更多信息,请参见继承。

当使用扩展方法扩展无法更改其源代码的类型时,存在这样的风险,即类型实现的更改将导致扩展方法中断。

如果确实为给定类型实现扩展方法,请记住以下几点:

  • 如果扩展方法具有与类型中定义的方法相同的签名,则永远不会调用该方法。
  • 扩展方法在名称空间级别纳入范围。例如,如果您有多个静态类,这些静态类在名为的单个命名空间中包含扩展方法Extensions,则它们将全部通过using Extensions;指令引入作用域。

总而言之,扩展方法旨在将实例方法添加到特定类型,即使开发人员无法直接这样做也是如此。而且由于实例方法将始终覆盖扩展方法(如果存在)(如果使用实例方法语法调用),因此当您不能直接添加方法或扩展类时才应这样做。

换句话说,扩展方法应该像实例方法一样工作,因为它可能最终被某些客户端作为实例方法。并且由于实例方法应该在被调用的对象为的情况下抛出null,所以扩展方法也应该抛出该异常。


*作为一个旁注,这恰恰是LINQ设计师面临的情况:发布C#3.0时,在其集合和循环中已经有数百万个客户在使用System.Collections.IEnumerable和。这些类返回的对象,其只用了两种方法和,因此添加任何额外的要求实例方法,如,等,将被打破,这些数以百万计的客户。因此,为了提供这种功能(特别是因为它可以在以下方面来实现并且相对容易地)时,它们释放它作为扩展方法,其可应用到任何现有System.Collections.Generic.IEnumerable<T>foreachIEnumeratorCurrentMoveNextCountAnyCurrentMoveNextIEnumerable实例,也可以由类以更有效的方式实现。如果C#的设计师决定在第一天发布LINQ,它将作为的实例方法提供IEnumerable,他们可能会设计某种系统来提供这些方法的默认接口实现。


0

我认为这里的重点是,通过返回false而不是引发异常,您正在混淆可能与将来的代码阅读者/修改者相关的信息。

如果列表可能为空,那么我建议为此使用一个单独的逻辑路径,否则将来有人可能会向if的else {}中添加一些基于列表的逻辑(如add),从而导致他们没有理由预测的意外异常。

可读性和可维护性胜过“我必须写一个额外的条件”。


0

通常,我编写的大多数代码都假定调用者负责不向我提供null数据,这主要是出于对Null拒绝(以及其他类似帖子)中概述的原因。null的概念通常没有得到很好的理解,因此,建议您尽可能实际地初始化变量。假设您使用的是合理的API,理想情况下,您永远不应返回空值,因此您永远不必检查空值。正如其他答案所指出的那样,无效点是要确保您有继续使用的有效对象(例如“袋子”)。这不是的功能Any,而是语言的功能本身。您不能对空对象执行大多数操作,因为它不存在。就像我要您开车开车去商店里一样,除了我自己没有车外,所以您无法使用我的汽车开车去商店里。对实际上不存在的对象执行操作是没有意义的。

在某些情况下,可能会使用null,例如,当您实际上没有所需的信息时(例如,如果我问过您的年龄,那么此时您最有可能选择的答案是“我不知道”,这是一个空值)。但是,在大多数情况下,至少应知道变量是否已初始化。如果您不这样做,那么我建议您需要收紧代码。在任何程序或函数中,我要做的第一件事就是在使用值之前对其进行初始化。很少需要检查空值,因为我可以保证变量不为空。这是一个很好的习惯,而且您记住初始化变量的频率越高,检查空值的频率就越低。

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.