为什么在collection为null时.NET foreach循环会引发NullRefException?


231

因此,我经常遇到这种情况... Do.Something(...)返回一个null集合,如下所示:

int[] returnArray = Do.Something(...);

然后,我尝试像这样使用此集合:

foreach (int i in returnArray)
{
    // do some more stuff
}

我很好奇,为什么foreach循环不能对null集合进行操作?在我看来,0迭代将使用null集合来执行是合理的……相反,它抛出NullReferenceException。有人知道为什么会这样吗?

这很烦人,因为我使用的API尚不清楚确切返回的API,所以我if (someCollection != null)到处都是...

编辑:谢谢大家对foreach使用的解释,GetEnumerator如果没有枚举数可以获取,则foreach将失败。我想我在问为什么语言/运行时不能或不会在获取枚举数之前做空检查。在我看来,这种行为仍然可以很好地定义。


1
将数组称为集合是有问题的。但是也许我只是老学校。
Robaticus 2010年

是的,我同意...我什至不知道为什么此代码库中有这么多方法返回数组
x_x

4
我以同样的理由推测,如果给定值,则C#中的所有语句将成为无操作对象将得到很好的定义null。您是否还建议将其用于正义foreach循环或其他语句?

7
@ Ken ...我只是在考虑foreach循环,因为对我来说,程序员看来很明显,如果集合为空或不存在,则什么也不会发生
Polaris878 2010年

Answers:


251

好吧,简短的答案是“因为这是编译器设计者设计它的方式”。但是实际上,您的集合对象为null,因此编译器无法让枚举器遍历集合。

如果您确实需要执行以下操作,请尝试使用null合并运算符:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}

3
请原谅我的无知,但这有效吗?是否不会在每次迭代中进行比较?
user919426 '16

20
我不相信 查看生成的IL,循环在null比较之后。
Robaticus

10
神圣的坏事...有时您必须查看IL,以查看编译器在做什么以找出是否有效率降低。User919426询问是否对每次迭代进行检查。尽管答案可能对某些人来说是显而易见的,但并不是所有人都明白,并且提示使用IL可以告诉您编译器在做什么,这将有助于人们将来自食其果。
Robaticus

2
@Robaticus(甚至以后为什么),IL也会显示原因,因为规范是这样说的。语法糖(又名foreach)的扩展是要评估“ in”右侧的表达式并调用GetEnumerator结果
Rune FS

2
@RuneFS-完全是。了解规范或查看IL是找出“为什么”的一种方法。或评估两种不同的C#方法是否归结为相同的IL。从本质上讲,这就是我对以上Shimmy的观点。
Robaticus

148

一个foreach循环中调用的GetEnumerator方法。
如果是集合null,则此方法调用将导致NullReferenceException

退还null集合是不好的做法。您的方法应该返回一个空集合。


7
我同意,应该总是返回空集合...但是我没有写这些方法:)
Polaris878 2010年

19
@Polaris,空合并运算符进行救援!int[] returnArray = Do.Something() ?? new int[] {};
JSBձոգչ2010年

2
或者:... ?? new int[0]

3
+1就像返回空集合而不是null的提示。谢谢。
Galilyou 2011年

1
我不同意一个不好的做法:请参见⇒如果某个函数失败,它可能会返回一个空集合-这是对构造函数的调用,内存分配,以及可能要执行的一堆代码。您可以只返回«null»→显然只有一个代码要返回,而要检查的参数很短就是«null»。这只是一场表演。
Hi-Angel

47

空集合和对集合的空引用之间有很大的区别。

foreach内部使用时,这将调用IEnumerable的GetEnumerator()方法。当引用为null时,这将引发此异常。

但是,具有IEnumerable或完全是有效的IEnumerable<T>。在这种情况下,因为这是一个完全有效的方案,所以foreach不会“迭代”任何东西(因为该集合为空),但是也不会抛出该异常。


编辑:

就个人而言,如果您需要解决此问题,我建议您使用一种扩展方法:

public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> original)
{
     return original ?? Enumerable.Empty<T>();
}

然后,您可以致电:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}

3
是的,但是为什么不让each在获得枚举数之前进行空检查?
Polaris878 2010年

12
@ Polaris878:因为它从未打算与null集合一起使用。IMO,这是一件好事-因为空引用和空集合应该分别处理。如果您想解决此问题,可以采取以下方法:我将编辑以显示其他选项...
Reed Copsey 2010年

1
@ Polaris878:我建议改写您的问题:“为什么运行时应该在获取枚举数之前进行空检查?”
Reed Copsey 2010年

我想我在问“为什么不呢?” 大声笑似乎仍然可以很好地定义其行为
Polaris878 2010年

2
@ Polaris878:我想,按照我的想法,为集合返回null是一个错误。现在,在这种情况下,运行时为您提供了有意义的异常,但是如果您不喜欢这种行为,则很容易解决(即:以上)。如果编译器向您隐藏了此内容,则您将在运行时丢失错误检查,但没有办法“关闭它” ...
Reed Copsey 2010年

12

它正在回答很久以前,但是我试图以以下方式做到这一点,只是避免了空指针异常,这可能对使用C#空检查运算符?的人有用。

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });

@kjbartel在一年多之前就击败了您(在“ stackoverflow.com/a/32134295/401246 ”上)。;)这是最好的解决方案,因为它不会:a)导致性能下降(即使不这样做null),将整个循环推广到LCD上Enumerable(如使用??),b)需要向每个项目添加扩展方法,和c)首先需要避免使用null IEnumerables(Pffft!Puh-LEAZE!SMH。)。
汤姆(Tom),

10

解决此问题的另一种扩展方法:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
    if(items == null) return;
    foreach (var item in items) action(item);
}

以几种方式消费:

(1)使用以下方法T

returnArray.ForEach(Console.WriteLine);

(2)带有表达式:

returnArray.ForEach(i => UpdateStatus(string.Format("{0}% complete", i)));

(3)用多行匿名方法

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});

仅在第三个示例中缺少右括号。否则,可以以有趣的方式(例如循环,反转,跳跃等)进一步扩展漂亮的代码。感谢分享。
拉拉

感谢您提供如此精彩的代码,但是我不明白第一种方法,为什么您将console.writeline作为参数传递,尽管它打印了数组元素。但是却不明白
Ajay Singh

@AjaySingh Console.WriteLine只是采用一个参数(an Action<T>)的方法的示例。项目1、2和3显示了将功能传递给.ForEach扩展方法的示例。
杰伊(Jay)

@ kjbartel的回答(在“ stackoverflow.com/a/32134295/401246 ”是最好的解决方案,因为它没有:一)涉及的(即使没有性能下降null)整个循环推广到的LCD Enumerable(使用??会),b)要求向每个项目添加扩展方法,或c)要求避免以null IEnumerables(Pffft!Puh-LEAZE!SMH。)开头(cuz,null表示不适用,而空列表表示,它是适用的,但是目前,好!为!(即,示例可能具有不适用于非销售的佣金,或针对销售而言为空的佣金)。
汤姆(Tom)

5

只需编写扩展方法即可为您提供帮助:

public static class Extensions
{
   public static void ForEachWithNull<T>(this IEnumerable<T> source, Action<T> action)
   {
      if(source == null)
      {
         return;
      }

      foreach(var item in source)
      {
         action(item);
      }
   }
}

5

因为空集合与空集合不是一回事。空集合是没有元素的集合对象。空集合是不存在的对象。

您可以尝试以下方法:声明两个任意类型的集合。通常将其初始化为空,然后为另一个赋值null。然后尝试将一个对象添加到两个集合中,看看会发生什么。


3

这是的错Do.Something()。此处的最佳做法是返回大小为0(可能)而不是null的数组。


2

因为幕后foreach获取了一个枚举器,所以等效于此:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}

2
所以?为什么不能先检查它是否为null并跳过循环?AKA,扩展方法到底显示了什么?问题是,如果为null还是抛出异常,默认情况下是否最好跳过循环?我认为最好跳过!似乎空容器应该跳过而不是循环,因为如果容器为非空,则循环是要执行某些操作的。
AbstractDissonance

@AbstractDissonance您可以对所有null引用进行相同的争论,例如在访问成员时。通常,这是一个错误,如果不是这样,那么使用另一个用户提供的扩展方法(例如,扩展方法)来处理该错误就足够了。
卢塞罗

1
我不这么认为。foreach用于对集合进行操作,与直接引用空对象不同。虽然有人可能会争论相同,但我敢打赌,如果您分析了世界上所有的代码,您将使大多数foreach循环在其前面都具有某种类型的null检查,仅当集合为“ null”时才绕过循环(这是因此视为空)。我认为没有人认为将null集合循环作为他们想要的东西,而是如果该集合为null,则宁愿忽略循环。也许可以使用foreach?(C中的var x)。
AbstractDissonance

我主要要说明的一点是,它会在代码中造成一些麻烦,因为每次都必须无缘无故地进行检查。这些扩展当然可以使用,但是可以添加一种语言功能来避免这些问题,而不会带来太多问题。(主要是我认为当前方法会产生隐藏的错误,因为程序员可能会忘记放置检查并因此产生了异常...因为他希望检查在循环之前的其他位置发生,或者正在考虑将其预先初始化(它可能会或可能已经改变),但是在这两种原因,结果会是一样的,如果空。
AbstractDissonance

@AbstractDissonance好吧,通过适当的静态分析,您知道哪里可以有null,哪里没有null。如果您不希望出现错误,那么最好失败而不是默默地忽略恕我直言的问题(本着快速失败的精神)。因此,我认为这是正确的行为。
卢塞罗

1

我认为通过此处提供的答案可以很清楚地解释为什么引发异常。我只是想补充我平常与theese收藏品合作的方式。因为有时候,我使用该集合的次数要多于一次,并且每次都必须测试是否为null。为避免这种情况,我执行以下操作:

    var returnArray = DoSomething() ?? Enumerable.Empty<int>();

    foreach (int i in returnArray)
    {
        // do some more stuff
    }

这样,我们可以尽可能多地使用集合,而不必担心异常,并且我们不会使用过多的条件语句来污染代码。

使用null检查运算符?.也是一种很好的方法。但是,如果是数组(例如问题中的示例),则应先将其转换为List:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });

2
ForEach我讨厌在代码库中做的事情之一就是转换为仅能访问该方法的列表。
huysentruitw

我同意...我尽量避免这种情况。:(
Alielson Piffer

-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
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.