Dlaczego pętla foreach platformy .NET generuje wyjątek NullRefException, gdy kolekcja jest pusta?


231

Często więc spotykam się z tą sytuacją ... gdzie Do.Something(...)zwraca kolekcję zerową, taką jak:

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

Następnie próbuję użyć tej kolekcji w następujący sposób:

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

Jestem tylko ciekawy, dlaczego pętla foreach nie może działać w zbiorze zerowym? Wydaje mi się logiczne, że 0 iteracji zostanie wykonanych z zerową kolekcją ... zamiast tego wyrzuca a NullReferenceException. Czy ktoś wie, dlaczego to może być?

Jest to denerwujące, ponieważ pracuję z interfejsami API, które nie są jasne, co dokładnie zwracają, więc kończę na if (someCollection != null)wszystkich ...

Edycja: Dziękuję wszystkim za wyjaśnienie, jakie foreachzastosowania, GetEnumeratora jeśli nie ma żadnego modułu wyliczającego, foreach się nie powiedzie. Wydaje mi się, że pytam, dlaczego język / środowisko wykonawcze nie może wykonać sprawdzenia zerowego przed pobraniem modułu wyliczającego. Wydaje mi się, że zachowanie nadal byłoby dobrze zdefiniowane.


1
Coś jest nie tak z nazwaniem tablicy kolekcją. Ale może jestem tylko starą szkołą.
Robaticus

Tak, zgadzam się ... Nie jestem nawet pewien, dlaczego tak wiele metod w tej bazie kodu zwraca tablice x_x
Polaris878

4
Podejrzewam, że z tego samego powodu byłoby dobrze zdefiniowane, aby wszystkie instrukcje w języku C # stały się brakiem operacji, gdy otrzyma nullwartość. Sugerujesz to dla samych foreachpętli lub innych instrukcji?
Ken

7
@ Ken ... Myślę tylko o zapętleniu pętli, ponieważ dla programisty wydaje mi się oczywiste, że nic by się nie stało, gdyby kolekcja była pusta lub nie istniała
Polaris878

Odpowiedzi:


251

Krótka odpowiedź brzmi „ponieważ tak zaprojektowali to projektanci kompilatorów”. Jednak realistycznie obiekt kolekcji jest pusty, więc kompilator nie może zapętlić modułu wyliczającego przez kolekcję.

Jeśli naprawdę musisz zrobić coś takiego, spróbuj zerowego operatora koalescencyjnego:

int[] array = null;

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

3
Proszę wybaczyć moją ignorancję, ale czy to jest skuteczne? Czy nie powoduje porównania każdej iteracji?
user919426,

20
Nie wierzę w to. Patrząc na wygenerowaną IL, pętla jest po porównaniu zerowym.
Robaticus,

10
Święta nekro ... Czasami musisz spojrzeć na IL, aby zobaczyć, co robi kompilator, aby dowiedzieć się, czy są jakieś spadki wydajności. Użytkownik919426 zapytał, czy sprawdził każdą iterację. Chociaż odpowiedź może być oczywista dla niektórych osób, nie jest ona oczywista dla wszystkich, a podpowiedź, że patrząc na IL powie ci, co robi kompilator, pomaga ludziom łowić siebie w przyszłości.
Robaticus

2
@Robaticus (nawet dlaczego później) IL wygląda na to, ponieważ tak mówi specyfikacja. Ekspansja cukru syntaktycznego (aka foreach) ma na celu ocenę wyrażenia po prawej stronie „in” i wywołanie GetEnumeratorwyniku
Rune FS

2
@RuneFS - dokładnie. Zrozumienie specyfikacji lub spojrzenie na IL to sposób na zrozumienie „dlaczego”. Lub ocenić, czy dwa różne podejścia C # sprowadzają się do tej samej IL. To był zasadniczo mój punkt do Shimmy powyżej.
Robaticus

148

foreachPętli wywołuje GetEnumeratormetodę.
Jeśli kolekcja jest null, to wywołanie tej metody powoduje, że NullReferenceException.

Zwracanie nullkolekcji jest złą praktyką ; twoje metody powinny zamiast tego zwrócić pustą kolekcję.


7
Zgadzam się, puste kolekcje powinny zawsze być zwracane ... jednak nie napisałem tych metod :)
Polaris878

19
@Polaris, zerowy operator koalescencyjny na ratunek! int[] returnArray = Do.Something() ?? new int[] {};
JSB ձոգչ

2
Lub: ... ?? new int[0].
Ken

3
+1 Podobnie jak końcówka zwracania pustych kolekcji zamiast wartości null. Dzięki.
Galilyou,

1
Nie zgadzam się na złą praktykę: patrz ⇒ jeśli funkcja zawiodła, może zwrócić pustą kolekcję - jest to wywołanie konstruktora, przydział pamięci i być może wiązka kodu do wykonania. Albo możesz po prostu zwrócić „null” → oczywiście jest tylko kod do zwrócenia, a bardzo krótki kod do sprawdzenia to argument „null”. To tylko przedstawienie.
Hi-Angel,

47

Istnieje duża różnica między pustą kolekcją a zerowym odwołaniem do kolekcji.

Gdy używasz foreachwewnętrznie, wywołuje to metodę GetEnumerator () IEnumerable . Gdy odwołanie ma wartość NULL, spowoduje to wyjątek.

Jednak jest całkowicie poprawne, aby mieć pusty IEnumerablelub IEnumerable<T>. W tym przypadku foreach nie będzie „iterował” nad niczym (ponieważ kolekcja jest pusta), ale również nie wyrzuci, ponieważ jest to całkowicie poprawny scenariusz.


Edytować:

Osobiście, jeśli chcesz obejść ten problem, polecam metodę rozszerzenia:

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

Następnie możesz po prostu zadzwonić:

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

3
Tak, ale DLACZEGO nie przewiduje, aby sprawdzić zerowanie przed uzyskaniem modułu wyliczającego?
Polaris878

12
@ Polaris878: Ponieważ nigdy nie był przeznaczony do użytku z kolekcją o wartości NULL. To jest, IMO, dobra rzecz - ponieważ zerowe odwołanie i pusta kolekcja powinny być traktowane osobno. Jeśli chcesz obejść ten problem, istnieją sposoby ... Przeedytuję, aby pokazać jeszcze jedną opcję ...
Reed Copsey

1
@ Polaris878: Sugerowałbym przeredagowanie pytania: „Dlaczego środowisko wykonawcze POWINIEN wykonać kontrolę zerową przed uzyskaniem modułu wyliczającego?”
Reed Copsey

Chyba pytam „dlaczego nie?” lol wygląda na to, że zachowanie byłoby nadal dobrze zdefiniowane
Polaris878

2
@ Polaris878: Myślę, że sposób, w jaki o tym myślę, zwracanie wartości null dla kolekcji jest błędem. Tak jak jest teraz, środowisko wykonawcze daje znaczący wyjątek w tym przypadku, ale łatwo jest obejść (tj. Powyżej), jeśli nie podoba ci się to zachowanie. Jeśli kompilator ukrył to przed tobą, straciłbyś sprawdzanie błędów w czasie wykonywania, ale nie byłoby sposobu, aby „wyłączyć” ...
Reed Copsey

12

To jest odpowiedź dawno temu, ale próbowałem to zrobić w następujący sposób, aby po prostu uniknąć wyjątku wskaźnika zerowego i może być przydatny dla kogoś, kto używa operatora C # zerowania?

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

@kjbartel pokonał cię ponad rok (na stronie „ stackoverflow.com/a/32134295/401246 ”). ;) Jest to najlepsze rozwiązanie, ponieważ nie: a) nie powoduje obniżenia wydajności (nawet jeśli nie null) uogólnienia całej pętli do wyświetlacza LCD Enumerable(jak przy użyciu ??), b) wymaga dodania metody rozszerzenia do każdego projektu, oraz c) na początku należy unikać null IEnumerables (Pffft! Puh-LEAZE! SMH.)
Tom

10

Inna metoda rozszerzenia do obejścia tego:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
    if(items == null) return;
    foreach (var item in items) action(item);
}

Spożywać na kilka sposobów:

(1) z metodą, która akceptuje T:

returnArray.ForEach(Console.WriteLine);

(2) z wyrażeniem:

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

(3) z wieloliniową anonimową metodą

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});

Brakuje nawiasu zamykającego w trzecim przykładzie. W przeciwnym razie piękny kod, który można rozszerzyć w interesujący sposób (dla pętli, cofania, przeskakiwania itp.). Dzięki za udostępnienie.
Lara,

Dzięki za tak wspaniały kod, ale nie zrozumiałem pierwszych metod, dlaczego przekazujesz parametr console.writeline jako parametr, chociaż drukuje on elementy tablicy. Ale nie rozumiem
Ajay Singh

@AjaySingh Console.WriteLinejest tylko przykładem metody, która przyjmuje jeden argument (an Action<T>). Pozycje 1, 2 i 3 pokazują przykłady przekazywania funkcji do .ForEachmetody rozszerzenia.
Jay

@ odpowiedź kjbartel użytkownika (w „ stackoverflow.com/a/32134295/401246 ” jest najlepszym rozwiązaniem, ponieważ nie: a) dotyczą degradacji wydajności (nawet jeśli nie null) uogólniając całą pętlę na wyświetlaczu LCD Enumerable(jak użycie ??byłoby ), b) wymagają dodania Metody rozszerzenia do każdego Projektu, lub c) wymagają uniknięcia null IEnumerables (Pffft! Puh-LEAZE! SMH.) na początek (cuz, nulloznacza nie dotyczy, a pusta lista oznacza, że ​​ma zastosowanie. ale jest obecnie, no cóż, pusty !, tj. zatrudnienie może mieć prowizje, które nie występują w przypadku sprzedaży innej niż sprzedaż lub puste w przypadku sprzedaży).
Tom

5

Po prostu napisz metodę rozszerzenia, która pomoże Ci:

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

Ponieważ kolekcja zerowa nie jest tym samym, co kolekcja pusta. Pusta kolekcja to obiekt kolekcji bez elementów; kolekcja zerowa jest nieistniejącym obiektem.

Oto coś do wypróbowania: Zadeklaruj dwie kolekcje dowolnego rodzaju. Zainicjuj jedną normalnie, aby była pusta, i przypisz drugiej wartość null. Następnie spróbuj dodać obiekt do obu kolekcji i zobacz, co się stanie.


3

To wina Do.Something(). Najlepszą praktyką byłoby zwrócenie tablicy o rozmiarze 0 (to możliwe) zamiast wartości null.


2

Ponieważ za kulisami foreachnabywa moduł wyliczający, równoważny z tym:

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

2
więc? Dlaczego nie może po prostu sprawdzić, czy najpierw jest zerowy i pominąć pętlę? AKA, dokładnie co pokazano w metodach rozszerzenia? Pytanie brzmi: czy lepiej jest domyślnie pominąć pętlę, jeśli ma wartość NULL, czy zgłosić wyjątek? Myślę, że lepiej jest pominąć! Wydaje się prawdopodobne, że puste pojemniki mają być pomijane, a nie zapętlane, ponieważ pętle mają coś zrobić, JEŻELI kontener nie jest pusty.
AbstractDissonance,

@AbstractDissonance Możesz argumentować tak samo ze wszystkimi nullreferencjami, np. Podczas uzyskiwania dostępu do członków. Zazwyczaj jest to błąd, a jeśli nie jest, to wystarczy go obsłużyć, na przykład metodą rozszerzenia, którą inny użytkownik podał jako odpowiedź.
Lucero,

1
Nie wydaje mi się Foreach ma działać na całej kolekcji i różni się od bezpośredniego odwoływania się do pustego obiektu. Chociaż można by argumentować tak samo, założę się, że jeśli przeanalizujesz cały kod na świecie, większość pętli foreach ma przed nimi jakieś kontrole zerowe tylko po to, aby ominąć pętlę, gdy kolekcja jest „null” (która jest stąd traktowane tak samo jak puste). Nie sądzę, aby ktokolwiek rozważał zapętlanie kolekcji zerowej jako coś, co chce, i wolałby po prostu zignorować pętlę, jeśli kolekcja jest pusta. Może raczej można użyć foreach? (Var x in C).
AbstractDissonance

Chodzi przede wszystkim o to, że tworzy on trochę śmiechu w kodzie, ponieważ trzeba to sprawdzać za każdym razem bez uzasadnionego powodu. Rozszerzenia oczywiście działają, ale można dodać funkcję języka, aby uniknąć tych problemów bez większego problemu. (głównie myślę, że obecna metoda powoduje ukryte błędy, ponieważ programista może zapomnieć umieścić czek i stąd wyjątek ... ponieważ albo spodziewa się, że sprawdzenie wystąpi gdzieś indziej przed pętlą, albo myśli, że został on wstępnie zainicjowany (co może lub może się zmienić), ale w obu przypadkach zachowanie byłoby takie samo, jak gdyby było puste
AbstractDissonance

@AbstractDissonance Cóż, przy odpowiedniej analizie statycznej wiesz, gdzie możesz mieć wartości zerowe, a gdzie nie. Jeśli dostaniesz zero, którego się nie spodziewasz, lepiej jest zawieść, zamiast cicho ignorować problemy IMHO (w duchu szybkiego upadku ). Dlatego uważam, że jest to prawidłowe zachowanie.
Lucero,

1

Myślę, że wyjaśnienie, dlaczego wyjątek jest zgłaszany, jest bardzo jasne w odpowiedziach tutaj. Chciałbym tylko uzupełnić sposób, w jaki zwykle pracuję z tymi kolekcjami. Ponieważ czasami używam kolekcji więcej niż jeden raz i za każdym razem muszę sprawdzać, czy jest pusta. Aby tego uniknąć, wykonuję następujące czynności:

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

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

W ten sposób możemy korzystać z kolekcji tak, jak chcemy, bez obawy wyjątku i nie zanieczyszczamy kodu nadmiernymi instrukcjami warunkowymi.

Świetnym ?.podejściem jest także użycie operatora zerowania. Ale w przypadku tablic (jak w przykładzie w pytaniu) należy go wcześniej przekształcić w List:

    int[] returnArray = DoSomething();

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

2
Konwersja na listę tylko w celu uzyskania dostępu do ForEachmetody jest jedną z rzeczy, których nienawidzę w bazie kodu.
huysentruitw

Zgadzam się ... Unikam tego w jak największym stopniu. :(
Alielson Piffer

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

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.