关于C#:当集合为空时,.NET foreach循环为什么会引发NullRefException?

Why does .NET foreach loop throw NullRefException when collection is null?

所以我经常遇到这种情况…其中Do.Something(...)返回一个空集合,如下所示:

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

然后,我试着这样使用这个集合:

1
2
3
4
foreach (int i in returnArray)
{
    // do some more stuff
}

我只是好奇,为什么foreach循环不能对空集合进行操作?在我看来,使用空集合执行0次迭代是合乎逻辑的…相反,它抛出了一个NullReferenceException。有人知道为什么会这样吗?

这很烦人,因为我使用的API并不清楚它们返回的确切内容,所以我最终在所有地方使用if (someCollection != null)……

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


好吧,简短的回答是"因为编译器设计人员就是这样设计的。"但实际上,集合对象是空的,所以编译器没有办法让枚举器在集合中循环。

如果确实需要这样做,请尝试使用空合并运算符:

1
2
3
4
5
6
    int[] array = null;

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


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

返回null集合是不好的做法;您的方法应该返回空集合。


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

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

但是,使用空的IEnumerableIEnumerable是完全有效的。在这种情况下,foreach不会"迭代"任何东西(因为集合是空的),但也不会抛出,因为这是一个完全有效的场景。

编辑:

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

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

然后你可以打电话给:

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


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

1
2
3
4
5
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的方法:

1
returnArray.ForEach(Console.WriteLine);

(二)有下列用语:

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

(3)多行匿名方法

1
2
3
4
5
6
7
int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});


这是很早以前的答案,但我尝试用以下方法来避免空指针异常,并且可能对使用C空检查运算符的人有用?.

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


因为空集合与空集合不同。空集合是没有元素的集合对象;空集合是不存在的对象。

这里有一些尝试:声明任意类型的两个集合。通常初始化一个,使其为空,并为另一个赋值null。然后尝试向两个集合中添加一个对象,看看会发生什么。


只需编写一个扩展方法来帮助您:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
      }
   }
}

Do.Something()的错。这里的最佳实践是返回一个大小为0的数组(这是可能的),而不是一个空数组。


因为在幕后,foreach获得了一个枚举器,相当于:

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


我认为对于为什么抛出异常的解释非常清楚,这里提供了答案。我只是想补充一下我通常处理废弃收藏品的方式。因为,有时,我会多次使用集合,每次都必须测试是否为空。为了避免出现这种情况,我会执行以下操作:

1
2
3
4
5
6
    var returnArray = DoSomething() ?? Enumerable.Empty<int>();

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

这样,我们就可以随心所欲地使用集合,而不必担心异常,也不会用过多的条件语句来争论代码。

使用空校验操作符?.也是一种很好的方法。但是,对于数组(如问题中的示例),应该在以下时间之前将其转换为列表:

1
2
3
4
5
6
    int[] returnArray = DoSomething();

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


1
2
3
4
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;