Почему .NET foreach loop выбрасывает NullRefException, когда коллекция имеет значение null?

Поэтому я часто сталкиваюсь с этой ситуацией... где Do.Something(...) возвращает нулевую коллекцию, например:

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

Затем я пытаюсь использовать эту коллекцию так:

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

Мне просто интересно, почему цикл foreach не работает в нулевой коллекции? Мне кажется логичным, что 0 итераций будут выполняться с нулевой коллекцией... вместо этого он выдает NullReferenceException. Кто-нибудь знает, почему это может быть?

Это раздражает, когда я работаю с API-интерфейсами, которые не совсем понятны тем, что они возвращают, поэтому я получаю везде if (someCollection != null)...

Изменить: Благодарим вас за объяснение, что foreach использует GetEnumerator, и если нет перечислителя, geteach будет терпеть неудачу. Наверное, я спрашиваю, почему язык/время выполнения не может или не будет выполнять нулевую проверку перед захватом перечислителя. Мне кажется, что поведение все равно будет хорошо определено.

Ответ 1

Ну, короткий ответ - "потому, что это разработали дизайнеры компилятора". Однако, реалистично, ваш объект коллекции имеет значение NULL, поэтому компилятор не может заставить перечислитель перебирать коллекцию.

Если вам действительно нужно сделать что-то вроде этого, попробуйте нулевой оператор коалесцирования:

    int[] array = null;

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

Ответ 2

Цикл

A foreach вызывает метод GetEnumerator.
Если коллекция null, этот вызов метода приводит к NullReferenceException.

Плохая практика возвращает коллекцию null; ваши методы должны возвращать пустую коллекцию.

Ответ 3

Существует большая разница между пустой коллекцией и пустой ссылкой на коллекцию.

Когда вы используете foreach внутреннего использования, это вызывает метод IEnumerable GetEnumerator(). Когда ссылка пуста, это вызовет это исключение.

Однако вполне допустимо иметь пустой 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
}

Ответ 4

Другой способ расширения для этого:

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

Ответ 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);
      }
   }
}

Ответ 6

Потому что нулевая коллекция - это не то же самое, что пустая коллекция. Пустая коллекция - это объект коллекции без элементов; нулевой набор является несуществующим объектом.

Здесь что-то попробовать: Объявите две коллекции любого рода. Инициализируйте его так, чтобы он был пустым, а другой присвойте значение null. Затем попробуйте добавить объект в обе коллекции и посмотреть, что произойдет.

Ответ 7

Ответ длинный, но я попытался сделать это следующим образом, чтобы просто исключить исключение нулевого указателя и может быть полезным для кого-то, кто использует оператор проверки нулевой последовательности С#.

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

Ответ 8

Это ошибка Do.Something(). Лучшей практикой здесь было бы вернуть массив размера 0 (что возможно) вместо нуля.

Ответ 9

Потому что за кулисами foreach получает перечислитель, эквивалентный этому:

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

Ответ 10

Я думаю, что объяснение того, почему выбрано исключение, очень ясен с ответами, представленными здесь. Я просто хочу дополнить то, как я обычно работаю с этими коллекциями. Потому что, несколько раз, я использую коллекцию более одного раза и должен каждый раз проверять значение null. Чтобы этого избежать, я делаю следующее:

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

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

Таким образом, мы можем использовать коллекцию столько, сколько хотим, не опасаясь исключения, и мы не будем интерпретировать код с чрезмерными условными утверждениями.

Использование оператора нулевой проверки ?. также является отличным подходом. Но в случае массивов (например, пример в вопросе) он должен быть преобразован в Список до:

    int[] returnArray = DoSomething();

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

Ответ 11

таким образом, вопрос действительно о "почему", и да, принятый ответ - это метатеоретически правильный ответ, но это не абсолютный правильный ответ. Также было дано много ответов для решения этой проблемы, но это все еще не решило проблему ОП, связанную с необходимостью включения дополнительного кода для его обработки. Поскольку эта проблема преследует меня, и я в некотором роде новичок в ASP.NET MVC, мне пришлось абсолютно точно понять, почему, чтобы я мог учесть эту "функцию" в будущем. Мне пришлось по-настоящему взглянуть на результаты отладки, чтобы понять, почему, и, обратив внимание, я могу рассказать вам настоящую причину.

Вот почему. При создании коллекции из запроса, например коллекции, созданной с использованием LINQ или EntityFramework, даже если в списке нет сущностей или элементов, он все равно имеет значение Count == 1, а значение первого индекса равно нулю.

Ответ 12

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

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