【问题标题】:Why does .NET foreach loop throw NullRefException when collection is null?为什么当集合为空时.NET foreach 循环会抛出 NullRefException?
【发布时间】:2010-06-21 20:09:34
【问题描述】:

所以我经常遇到这种情况...Do.Something(...) 返回一个空集合,像这样:

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

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

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

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

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

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

【问题讨论】:

  • 将数组称为集合感觉有些不对劲。但也许我只是老派。
  • 是的,我同意...我什至不确定为什么这个代码库中有这么多方法返回数组 x_x
  • 我想通过同样的推理,当给定 null 值时,对于 C# 中的 all 语句将成为无操作是明确定义的。您是否建议仅针对 foreach 循环或其他语句?
  • @Ken... 我只是在考虑 foreach 循环,因为对程序员来说,如果集合为空或不存在,则似乎什么都不会发生

标签: c# .net


【解决方案1】:

嗯,简短的回答是“因为这是编译器设计者设计的方式”。但实际上,您的集合对象为空,因此编译器无法让枚举数循环遍历集合。

如果你真的需要做这样的事情,试试空合并运算符:

int[] array = null;

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

【讨论】:

  • 请原谅我的无知,但这有效吗?它不会导致每次迭代都进行比较吗?
  • 我不这么认为。查看生成的 IL,循环在 is null 比较之后。
  • Holy necro... 有时您必须查看 IL 以了解编译器在做什么才能确定是否存在任何效率问题。 User919426 询问它是否对每次迭代都进行了检查。虽然答案对某些人来说可能是显而易见的,但对每个人来说并不是显而易见的,并且提供的提示是,查看 IL 会告诉你编译器在做什么,这有助于人们在未来自己钓鱼。
  • @Robaticus(甚至为什么后来)IL 看起来是为什么,因为规范是这样说的。语法糖(又名foreach)的扩展是对“in”右侧的表达式求值,并在结果上调用GetEnumerator
  • @RuneFS - 完全正确。了解规范或查看 IL 是找出“为什么”的一种方法。或者评估两种不同的 C# 方法是否归结为相同的 IL。基本上,这就是我对上述 Shimmy 的看法。
【解决方案2】:

foreach 循环调用 GetEnumerator 方法。
如果集合是null,则此方法调用会产生NullReferenceException

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

【讨论】:

  • 我同意,应该始终返回空集合...但是我没有编写这些方法:)
  • @Polaris,空合并运算符来救援! int[] returnArray = Do.Something() ?? new int[] {};
  • +1 就像返回空集合而不是 null 的提示一样。谢谢。
  • 我不同意一个不好的做法:参见 ⇒ 如果一个函数失败,它可能会返回一个空集合——它是对构造函数、内存分配的调用,可能还有一堆要执行的代码。要么你可以只返回«null»→显然只有一个代码要返回,一个非常短的代码要检查是参数是«null»。这只是一场表演。
  • 我还要补充一点:无论哪种方式,合并运算符都会创建一个空列表。但这已经是用户的决定:如果有人在某处调用函数,例如在 GUI 内部,性能并不重要,他们可能会决定这样做。但是如果他们正在做一些需要性能的事情,他们只需插入一个检查“如果结果不为空,请执行 foreach”。虽然为了高性能通常使用 C++ :Ь
【解决方案3】:

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

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

但是,有一个空的IEnumerableIEnumerable&lt;T&gt; 是完全有效的。在这种情况下,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
}

【讨论】:

  • 是的,但是为什么foreach在获取枚举数之前不做空检查?
  • @Polaris878:因为它从未打算与空集合一起使用。这是,IMO,一件好事 - 因为空引用和空集合应该分开处理。如果你想解决这个问题,有办法......我将编辑以显示另一个选项......
  • @Polaris878:我建议改写你的问题:“为什么运行时应该在获取枚举数之前进行空检查?”
  • @Polaris878:我想,按照我的想法,为集合返回 null 是一个错误。现在的方式是,运行时在这种情况下为您提供了一个有意义的异常,但如果您不喜欢这种行为,它很容易解决(即:上面)。如果编译器对您隐藏了这一点,您将在运行时丢失错误检查,但没有办法“将其关闭”...
  • 很好的解决方案,在这里作为扩展实现。
【解决方案4】:

这是很久以前的回答,但我尝试通过以下方式执行此操作,以避免空指针异常,并且可能对使用 C# 空检查运算符的人有用?。

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

【讨论】:

  • @kjbartel 比你领先一年多(在“stackoverflow.com/a/32134295/401246”)。 ;) 这是最好的解决方案,因为它不会:a) 涉及性能下降(即使不是null)将整个循环推广到Enumerable 的 LCD(如使用?? 那样),b)需要向每个项目添加扩展方法,并且 c) 需要避免 null IEnumerables (Pffft! Puh-LEAZE! SMH.) 开始。
【解决方案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 的方法:

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);
});

【讨论】:

  • 在第三个示例中缺少右括号。否则,漂亮的代码可以以有趣的方式进一步扩展(for循环、反转、跳跃等)。感谢分享。
  • 感谢这么精彩的代码,但我不明白第一种方法,为什么你传递 console.writeline 作为参数,虽然它打印数组元素。但不明白
  • @AjaySingh Console.WriteLine 只是采用一个参数(Action&lt;T&gt;)的方法的一个示例。第 1、2 和 3 项显示了将函数传递给 .ForEach 扩展方法的示例。
  • @kjbartel 的回答(在“stackoverflow.com/a/32134295/401246”是最好的解决方案,因为它不会:a)涉及性能下降(即使不是null)将整个循环推广到 LCD的Enumerable(与使用?? 一样),b)需要向每个项目添加扩展方法,或c)需要避免nullIEnumerables(Pffft!Puh-LEAZE!SMH。)以(因为,null 意味着 N/A,而空列表意味着它是 appl. 但目前,嗯,empty!,即一个 Empl. 可能有对非销售或 N/A 不适用的佣金销售为空)。
【解决方案6】:

只需要写一个扩展方法来帮助你:

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);
      }
   }
}

【讨论】:

    【解决方案7】:

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

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

    【讨论】:

      【解决方案8】:

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

      【讨论】:

        【解决方案9】:

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

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

        【讨论】:

        • 所以?为什么不能先检查它是否为空并跳过循环? AKA,扩展方法中到底显示了什么?问题是,如果为 null 则默认跳过循环还是抛出异常更好?我觉得最好跳过!似乎空容器应该被跳过而不是循环,因为循环是为了做一些事情 IF 容器是非空的。
        • @AbstractDissonance 您可以对所有 null 引用提出相同的观点,例如访问成员时。通常这是一个错误,如果不是,那么处理这个问题就足够简单了,例如使用另一个用户提供的扩展方法作为答案。
        • 我不这么认为。 foreach 旨在对集合进行操作,与直接引用空对象不同。虽然有人可能会这样说,但我敢打赌,如果你分析了世界上所有的代码,你会发现大多数 foreach 循环在它们前面都有某种类型的 null 检查,只是在集合为“null”时绕过循环(即因此视为空)。我认为没有人认为循环一个空集合是他们想要的,并且宁愿在集合为空时简单地忽略循环。也许,更确切地说,可以使用 foreach?(var x in C)。
        • 我主要想说的是,它会在代码中产生一些垃圾,因为每次都必须无缘无故地检查。当然,扩展是有效的,但是可以添加一个语言功能来避免这些事情,而不会出现太大问题。 (主要是我认为当前方法会产生隐藏的错误,因为程序员可能会忘记进行检查,因此会出现异常......因为他希望检查发生在循环之前的其他地方,或者认为它是预先初始化的(其中它可能会或可能已经改变)。但无论哪种原因,行为都会与空的相同。
        • @AbstractDissonance 好吧,通过一些适当的静态分析,您知道哪里可以有空值,哪里没有。如果你得到一个你不期望的空值,最好失败而不是默默地忽略问题恕我直言(本着failing fast的精神)。因此我觉得这是正确的行为。
        【解决方案10】:

        我认为这里提供的答案非常清楚地解释了为什么抛出异常。我只是想补充一下我通常使用这些系列的方式。因为,有时,我多次使用该集合,并且每次都必须测试是否为 null。为避免这种情况,我执行以下操作:

            var returnArray = DoSomething() ?? Enumerable.Empty<int>();
        
            foreach (int i in returnArray)
            {
                // do some more stuff
            }
        

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

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

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

        【讨论】:

        • 为了访问ForEach 方法而转换为列表是我在代码库中讨厌的事情之一。
        • 我同意...我尽可能避免这种情况。 :(
        【解决方案11】:
        SPListItem item;
        DataRow dr = datatable.NewRow();
        
        dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
        

        【讨论】:

          猜你喜欢
          • 2023-03-24
          • 2020-11-02
          • 1970-01-01
          • 2019-08-21
          • 2016-04-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-02-04
          相关资源
          最近更新 更多