Чому цикл .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і якщо немає нумератора, передбачення не вдасться. Я думаю, я запитую, чому мова / час виконання не може чи не зробить нульову перевірку перед тим, як схопити перелік. Мені здається, що поведінку все-таки було б чітко визначено.


1
Щось не так, коли ви називаєте масив колекцією. Але, можливо, я просто стара школа.
Робатикус

Так, я згоден ... Я навіть не впевнений, чому так багато методів у цій базовій коді повертають масиви x_x
Polaris878

4
Я припускаю, що з тих же міркувань було б чітко визначено, що всі твердження в C # стають невідповідними, коли дається nullзначення. Чи пропонуєте ви це лише для foreachциклів чи інших тверджень?
Кен

7
@ Кен ... Я думаю, що я просто передбачу петлі, тому що програмісту мені здається, що нічого не станеться, якщо колекція порожня або неіснуюча
Polaris878,

Відповіді:


251

Що ж, коротка відповідь - це тому, що це спроектували дизайнери-компілятори. Реально, однак, об'єкт колекції є нульовим, тому компілятор не може змусити перераховувати колекцію перечислювача.

Якщо вам дійсно потрібно зробити щось подібне, спробуйте оператор нульового згортання:

int[] array = null;

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

3
Вибачте, будь ласка, про моє незнання, але чи це ефективно? Чи це не призводить до порівняння кожної ітерації?
user919426

20
Я не вірю в це. Дивлячись на створений ІР, цикл знаходиться після порівняння з нулем.
Робатикус

10
Святий некро ... Іноді вам доведеться подивитися на ІЛ, щоб побачити, що робить компілятор, щоб з’ясувати, чи є ефективні хіти. Користувач919426 запитав, чи робить це перевірку для кожної ітерації. Хоча відповідь може бути очевидною для деяких людей, вона не очевидна для всіх, і якщо надати підказку, що перегляд ІЛ скаже вам, що робить компілятор, допомагає людям шукати себе в майбутньому.
Робатикус

2
@ Робатикус (навіть чому пізніше) ІЛ виглядає саме так, тому що специфікація так говорить. Розширення синтаксичного цукру (aka foreach) полягає в оцінці виразу з правого боку "в" і виклику GetEnumeratorрезультату
Rune FS

2
@RuneFS - точно. Розуміння специфікації або перегляд ІЛ - це спосіб з'ясувати "чому". Або оцінити, чи зводиться два різних підходи C # до одного ІЛ. Це, по суті, мій погляд на Шиммі вище.
Робатикус

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
Так, але ЧОМУ не передбачає зробити нульову перевірку перед тим, як отримати нумератор?
Polaris878

12
@ Polaris878: Тому що він ніколи не був призначений для використання з нульовою колекцією. Це, ІМО, гарна річ - адже нульова посилання та порожня колекція повинні розглядатися окремо. Якщо ви хочете обійтися цим, є способи .. Я редагую, щоб показати ще один варіант ...
Reed Copsey

1
@ Polaris878: Я б запропонував переробити ваше запитання: "Чому ДОЛЖЕН би час виконання зробити нульову перевірку, перш ніж отримати нумератор?"
Рід Копсей

Я думаю, я запитую "чому б і ні?" хай здається, що поведінка все-таки буде чітко визначена
Polaris878

2
@ Polaris878: Я думаю, що я думаю про це, повернення нуля для колекції - це помилка. Так, як зараз, час виконання дає вагомий виняток у цьому випадку, але легко обійтися (тобто: вище), якщо вам не подобається така поведінка. Якщо компілятор приховав це від вас, ви втратите перевірку помилок під час виконання, але не було б способу "вимкнути це" ...
Reed Copsey

12

Це відповідь довгий час назад, але я намагався зробити це наступним чином, щоб просто уникнути виключення нульових покажчиків і може бути корисним для тих, хто використовує оператора C # null check ?.

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

@kjbartel перешкодив вам досягти цього понад рік (за адресою " stackoverflow.com/a/32134295/401246 "). ;) Це найкраще рішення, оскільки воно не: а) передбачає погіршення продуктивності (навіть коли немає null) узагальнення цілого циклу на РК Enumerable(як це ??було б використано ); б) вимагає додавання методу розширення до кожного проекту, і в) вимагати уникати null IEnumerables (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);
});

Просто не вистачає дужок, що закриваються, у 3-му прикладі. В іншому випадку гарний код, який можна продовжити цікавими способами (для циклів, обертання назад, стрибків тощо). Дякую, що поділились.
Лара

Дякую за такий чудовий код, але я не зрозумів перших методів, чому ви передаєте console.writeline як параметр, хоча його друк елементів масиву. Але не зрозумів
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.) для початку (бо nullозначає, що N / A, тоді як порожній список означає, що це додатково, але це в даний час добре, порожньо !, тобто, експл. може мати комісії, які не відповідають вимогам продажів, або порожні для продажів).
Том

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 collection - це неіснуючий об’єкт.

Ось що можна спробувати: Оголосіть дві колекції будь-якого типу. Один звичайно ініціалізуйте, щоб він був порожнім, а іншому призначте значення null. Потім спробуйте додати об’єкт до обох колекцій і подивитися, що відбувається.


3

Це вина Do.Something(). Найкращою практикою тут буде повернути масив розміром 0 (це можливо) замість нуля.


2

Тому що за лаштунками foreachнабуває перелік, рівнозначний цьому:

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

2
так? Чому він не може просто перевірити, чи він спочатку є нульовим, і пропустити цикл? AKA, що саме показано в методах розширення? Питання в тому, чи краще за замовчуванням пропустити цикл, якщо нуль, або викинути виняток? Я думаю, що краще пропустити! Здається, що нульові контейнери мають бути пропущеними, а не циклічними, оскільки петлі мають на меті робити щось, якщо контейнер не є нульовим.
AbstractDissonance

@Ab абстрактDissonance Ви можете стверджувати те саме з усіма nullпосиланнями, наприклад, при доступі до членів. Зазвичай це помилка, і якщо це не так, досить просто впоратися з цим, наприклад, методом розширення, який інший користувач надав у якості відповіді.
Lucero

1
Я не думаю, що так. Foreach призначений для роботи над колекцією і відрізняється від прямого посилання на нульовий об'єкт. Хоча можна було б стверджувати те саме, я думаю, що якби ви проаналізували весь код у світі, у вас було б більшість циклів передбачення, які мають нульові перевірки перед ними, лише щоб обійти цикл, коли колекція "null" (що є значить, ставиться так само, як до порожнього). Я не думаю, що хтось вважає перекидання нульової колекції чимось, що хоче, і скоріше просто ігнорує цикл, якщо колекція є нульовою. Можливо, скоріше, можна було б використовувати передбачення? (Var x in C).
AbstractDissonance

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

@Ab абстрактDissonance Що ж, за допомогою належного статичного аналізу ви знаєте, де у вас можуть бути нулі, а де ні. Якщо ви отримаєте нуль там, де вас не очікує, краще провалитися, а не мовчки ігнорувати проблеми ІМХО (в дусі швидко провалюватися ). Тому я відчуваю, що це правильна поведінка.
Lucero

1

Я думаю, що пояснення, чому викинуто виняток, є дуже зрозумілим з наведених тут відповідей. Я просто хочу доповнити те, як я зазвичай працюю з цими колекціями. Тому що, деколи, я використовую колекцію більше, ніж один раз, і щоразу доводиться перевіряти, чи є нульовим. Щоб уникнути цього, я роблю наступне:

    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

Я згоден ... Я уникаю цього, наскільки це можливо. :(
Аліельсон Піффер

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

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.