Почему цикл .NET foreach генерирует исключение NullRefException, когда коллекция пуста?


231

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

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

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

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

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

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

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


1
Что-то не так в вызове массива как коллекции. Но, может быть, я просто старая школа.
Робатик

Да, я согласен ... Я даже не уверен, почему так много методов в этой кодовой базе возвращают массивы x_x
Polaris878

4
Я полагаю, что по тем же соображениям было бы четко определено, чтобы все операторы в C # становились бездействующими при задании nullзначения. Вы предлагаете это только для foreachциклов или других утверждений?
Кен

7
@ Кен ... Я думаю, что просто циклы foreach, потому что для программиста кажется очевидным, что ничего не произойдет, если коллекция пуста или не существует
Polaris878

Ответы:


251

Ну, краткий ответ - «потому что так разработчики компилятора разработали его». Реально, однако, ваш объект коллекции имеет значение null, поэтому компилятор не может заставить перечислитель перебрать коллекцию.

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

int[] array = null;

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

3
Прошу прощения за мое невежество, но эффективно ли это? Разве это не приводит к сравнению на каждой итерации?
user919426

20
Я не верю в это. Глядя на сгенерированный IL, цикл после нулевого сравнения.
Robaticus

10
Святой некро ... Иногда приходится смотреть на IL, чтобы увидеть, что делает компилятор, чтобы выяснить, есть ли какие-нибудь ошибки эффективности. Пользователь 919426 спросил, выполняет ли он проверку для каждой итерации. Хотя ответ может быть очевиден для некоторых людей, он не очевиден для всех, и дает подсказку, что взгляд на IL скажет вам, что делает компилятор, поможет людям ловить рыбу для себя в будущем.
Robaticus

2
@Robaticus (даже почему позже) IL выглядит именно так, потому что в спецификации так сказано. Расширение синтаксического сахара (aka foreach) заключается в том, чтобы оценить выражение с правой стороны слова «in» и вызвать GetEnumeratorрезультат
Rune FS

2
@RuneFS - точно. Понимание спецификации или взгляд на IL - это способ выяснить «почему». Или оценить, сводятся ли два разных подхода C # к одному и тому же IL. Это было, по сути, моей точкой зрения на Шимми выше.
Robaticus

148

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

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


7
Я согласен, пустые коллекции всегда нужно возвращать ... однако я не писал эти методы :)
Polaris878

19
@Polaris, ноль объединяющий оператор на помощь! int[] returnArray = Do.Something() ?? new int[] {};
JSB գչոգչ

2
Или: ... ?? new int[0].
Кен

3
+1 Как совет возврата пустых коллекций вместо нуля. Спасибо.
Галилу

1
Я не согласен с плохой практикой: смотрите ⇒ если функция потерпела неудачу, она может либо вернуть пустую коллекцию - это вызов конструктора, выделение памяти и, возможно, набор кода, который нужно выполнить. Либо вы можете просто вернуть «null» → очевидно, есть только код для возврата и очень короткий код, чтобы проверить, является ли аргумент «null». Это просто спектакль.
Привет, Ангел,

47

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

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

Однако вполне допустимо иметь пустой 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
Да, но ПОЧЕМУ не foreach не делает нулевую проверку перед получением перечислителя?
Polaris878

12
@ Polaris878: потому что он никогда не предназначался для использования с нулевой коллекцией. Это, IMO, хорошая вещь - поскольку нулевая ссылка и пустая коллекция должны обрабатываться отдельно. Если вы хотите обойти это, есть способы ... Я отредактирую, чтобы показать еще один вариант ...
Рид Копси

1
@ Polaris878: Я бы предложил переписать ваш вопрос: «Почему ДОЛЖНА среда выполнения делать нулевую проверку перед получением счетчика?»
Рид Копси

Я думаю, я спрашиваю "почему бы и нет?" лол, похоже, что поведение все еще будет хорошо определено
Polaris878

2
@ Polaris878: Полагаю, возвращать значение NULL для коллекции - это ошибка. Как это сейчас, среда выполнения дает вам значимое исключение в этом случае, но это легко обойти (т.е. выше), если вам не нравится это поведение. Если бы компилятор скрыл это от вас, вы потеряли бы проверку ошибок во время выполнения, но не было бы никакого способа «выключить это» ...
Рид Копси

12

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

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

@kjbartel побил вас этим больше года (на « stackoverflow.com/a/32134295/401246 »). ;) Это лучшее решение, потому что оно: а) не приводит к снижению производительности (даже если нет null) обобщения всего цикла на ЖК-дисплей Enumerable(как при использовании ??), б) требует добавления метода расширения для каждого проекта, и c) null IEnumerableдля начала требуется избегать s (Pffft! Puh-LEAZE! SMH.).
Том

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 в качестве параметра, хотя он печатает массив elements.but не понял
Ajay Singh

@AjaySingh Console.WriteLine- это просто пример метода, который принимает один аргумент (an Action<T>). Пункты 1, 2 и 3 показывают примеры передачи функций в .ForEachметод расширения.
Джей

@ ответ kjbartel (по крайней « stackoverflow.com/a/32134295/401246 » является лучшим решением, потому что он не делает: а) включать ухудшение характеристик (даже если они не null) , обобщающей весь цикл на ЖК - дисплее Enumerable(как использование ??будет ), b) требуют добавления метода расширения для каждого проекта, или c) требуют избегать null IEnumerables (Pffft! Puh-LEAZE! SMH.) для начала (cuz, nullозначает N / A, тогда как пустой список означает, что он применим, но в настоящее время, ну, пусто !, т. е. у Empl. могут быть комиссии, которые не применимы для не-продаж или пустые для продаж).
Том

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(). В этом случае рекомендуется возвращать массив нулевого размера (это возможно) вместо нуля.


2

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

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

2
так? Почему он не может просто проверить, является ли он нулевым, и пропустить цикл? Ака, именно то, что показано в методах расширения? Вопрос в том, лучше ли по умолчанию пропустить цикл, если он нулевой, или вызвать исключение? Я думаю, что лучше пропустить! Кажется вероятным, что нулевые контейнеры должны быть пропущены, а не зациклены, поскольку циклы должны делать что-то, если контейнер ненулевой.
AbstractDissonance

@AbstractDissonance Вы можете утверждать то же самое со всеми nullссылками, например, при доступе к членам. Обычно это ошибка, а если нет, то достаточно просто обработать это, например, с помощью метода расширения, который другой пользователь предоставил в качестве ответа.
Лусеро

1
Я так не думаю. Функция foreach предназначена для работы над коллекцией и отличается от прямой ссылки на нулевой объект. Хотя можно утверждать то же самое, я готов поспорить, что, если вы проанализируете весь код в мире, у вас будет большинство циклов foreach с пустыми проверками перед ними только для того, чтобы обойти цикл, когда коллекция является «нулевой» (что отсюда относились так же как к пустым). Я не думаю, что кто-то рассматривает циклическую обработку над нулевой коллекцией как что-то, что он хочет, и скорее просто проигнорирует цикл, если коллекция является нулевой. Может быть, лучше использовать foreach? (Var x в C).
AbstractDissonance

Главное, что я пытаюсь сделать, это то, что он создает немного мусора в коде, так как каждый раз нужно проверять без веской причины. Разумеется, расширения работают, но можно добавить языковую функцию, чтобы избежать этих проблем без особых проблем. (В основном, я думаю, что текущий метод создает скрытые ошибки, так как программист может забыть поставить проверку и, следовательно, исключение ... потому что либо он ожидает, что проверка произойдет где-то еще до цикла, либо думает, что она была предварительно инициализирована (что оно может или могло измениться.) Но в любом случае поведение будет таким же, как если бы оно было пустым.
AbstractDissonance

@AbstractDissonance Хорошо, с помощью некоторого правильного статического анализа вы знаете, где у вас могут быть нулевые значения, а где нет. Если вы получаете ноль, где вы не ожидаете его, лучше потерпеть неудачу, чем молча игнорировать проблемы ИМХО (в духе быстрого провала ). Поэтому я чувствую, что это правильное поведение.
Лусеро

1

Я думаю, что объяснение, почему выбрасывается исключение, очень ясно с ответами, представленными здесь. Я просто хочу дополнить то, как я обычно работаю с этими коллекциями. Потому что, иногда, я использую коллекцию более одного раза, и мне приходится каждый раз проверять, имеет ли значение 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
    });

2
Преобразование в список только для того, чтобы получить доступ к ForEachметоду, является одной из вещей, которые я ненавижу в кодовой базе.
huysentruitw

Я согласен ... Я избегаю этого, насколько это возможно. :(
Алиэльсон Пиффер

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.