Warum löst die .NET foreach-Schleife eine NullRefException aus, wenn die Auflistung null ist?

231

Daher stoße ich häufig auf diese Situation ... in Do.Something(...)der eine Nullsammlung zurückgegeben wird, wie folgt:

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

Dann versuche ich, diese Sammlung so zu verwenden:

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

Ich bin nur neugierig, warum kann eine foreach-Schleife nicht mit einer Nullsammlung arbeiten? Es scheint mir logisch, dass 0 Iterationen mit einer Nullsammlung ausgeführt werden ... stattdessen wird a ausgelöst NullReferenceException. Weiß jemand warum das sein könnte?

Das ist ärgerlich, da ich mit APIs arbeite, bei denen nicht klar ist, was sie zurückgeben, sodass ich if (someCollection != null)überall ...

Bearbeiten: Vielen Dank an alle für die Erklärung dieser foreachVerwendung GetEnumeratorund wenn es keinen Enumerator gibt, der abgerufen werden kann, würde der foreach fehlschlagen. Ich frage mich wohl, warum die Sprache / Laufzeit keine Nullprüfung durchführen kann oder will, bevor ich den Enumerator greife. Es scheint mir, dass das Verhalten immer noch gut definiert wäre.

Polaris878
quelle
1
Es fühlt sich falsch an, ein Array als Sammlung zu bezeichnen. Aber vielleicht bin ich nur alte Schule.
Robaticus
Ja, ich stimme zu ... Ich bin mir nicht einmal sicher, warum so viele Methoden in dieser Codebasis Arrays zurückgeben x_x
Polaris878
4
Ich nehme an, dass es aus derselben Überlegung gut definiert wäre, wenn alle Anweisungen in C # zu No-Ops werden, wenn ein nullWert angegeben wird. Schlagen Sie dies auch nur für foreachSchleifen oder andere Anweisungen vor?
Ken
7
@ Ken ... Ich denke nur an jede Schleife, weil mir für den Programmierer klar erscheint, dass nichts passieren würde, wenn die Sammlung leer oder nicht vorhanden ist
Polaris878

Antworten:

251

Die kurze Antwort lautet: "So haben es die Compiler-Designer entworfen." Realistisch gesehen ist Ihr Sammlungsobjekt jedoch null, sodass der Compiler den Enumerator nicht dazu bringen kann, die Sammlung zu durchlaufen.

Wenn Sie so etwas wirklich tun müssen, versuchen Sie es mit dem Null-Koaleszenz-Operator:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}
Robaticus
quelle
3
Bitte entschuldigen Sie meine Unwissenheit, aber ist das effizient? Führt dies nicht zu einem Vergleich bei jeder Iteration?
user919426
20
Ich glaube nicht. Bei Betrachtung der generierten IL befindet sich die Schleife nach dem Nullvergleich.
Robaticus
10
Heiliger Nekro ... Manchmal muss man sich die IL ansehen, um zu sehen, was der Compiler tut, um herauszufinden, ob es Effizienzeinbußen gibt. User919426 hatte gefragt, ob die Überprüfung für jede Iteration durchgeführt wurde. Obwohl die Antwort für einige Leute offensichtlich sein mag, ist sie nicht für alle offensichtlich, und der Hinweis, dass ein Blick auf die IL Ihnen sagt, was der Compiler tut, hilft den Leuten, in Zukunft für sich selbst zu fischen.
Robaticus
2
@Robaticus (auch warum später) sieht die IL so aus, warum, weil die Spezifikation dies sagt. Die Erweiterung des syntaktischen Zuckers (auch bekannt als foreach) besteht darin, den Ausdruck auf der rechten Seite von "in" zu bewerten und GetEnumeratordas Ergebnis aufzurufen
Rune FS,
2
@ RunFS - genau. Das Verstehen der Spezifikation oder das Betrachten der IL ist eine Möglichkeit, das "Warum" herauszufinden. Oder um zu bewerten, ob zwei verschiedene C # -Ansätze auf dieselbe IL hinauslaufen. Das war im Wesentlichen mein Punkt an Shimmy oben.
Robaticus
148

Eine foreachSchleife ruft die GetEnumeratorMethode auf.
Wenn die Auflistung ist null, führt dieser Methodenaufruf zu a NullReferenceException.

Es ist eine schlechte Praxis, eine nullSammlung zurückzugeben. Ihre Methoden sollten stattdessen eine leere Sammlung zurückgeben.

SLaks
quelle
7
Ich bin damit einverstanden, leere Sammlungen sollten immer zurückgegeben werden ... aber ich habe diese Methoden nicht geschrieben :)
Polaris878
19
@Polaris, Null-Koaleszenz-Operator zur Rettung! int[] returnArray = Do.Something() ?? new int[] {};
JSB 21
2
Oder : ... ?? new int[0].
Ken
3
+1 Wie der Tipp, leere Sammlungen anstelle von null zurückzugeben. Vielen Dank.
Galilyou
1
Ich bin mit einer schlechten Vorgehensweise nicht einverstanden: Siehe ⇒ Wenn eine Funktion fehlschlägt, kann sie entweder eine leere Sammlung zurückgeben - es handelt sich um einen Aufruf des Konstruktors, eine Speicherzuweisung und möglicherweise um eine Reihe von Code, der ausgeführt werden soll. Entweder könnten Sie einfach «null» zurückgeben → offensichtlich muss nur ein Code zurückgegeben werden, und ein sehr kurzer Code, der überprüft werden muss, ist das Argument «null». Es ist nur eine Aufführung.
Hi-Angel
47

Es gibt einen großen Unterschied zwischen einer leeren Sammlung und einem Nullverweis auf eine Sammlung.

Wenn Sie foreachintern verwenden, ruft dies die GetEnumerator () -Methode des IEnumerable auf . Wenn die Referenz null ist, wird diese Ausnahme ausgelöst.

Es ist jedoch durchaus gültig, ein leeres IEnumerableoder zu haben IEnumerable<T>. In diesem Fall "iteriert" foreach nichts (da die Sammlung leer ist), wirft aber auch nicht, da dies ein absolut gültiges Szenario ist.


Bearbeiten:

Persönlich würde ich eine Erweiterungsmethode empfehlen, wenn Sie dies umgehen müssen:

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

Sie können dann einfach anrufen:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}
Reed Copsey
quelle
3
Ja, aber WARUM führt foreach keine Nullprüfung durch, bevor der Enumerator abgerufen wird?
Polaris878
12
@ Polaris878: Weil es nie für die Verwendung mit einer Nullsammlung gedacht war. Dies ist, IMO, eine gute Sache - da eine Nullreferenz und eine leere Sammlung getrennt behandelt werden sollten. Wenn Sie dies umgehen möchten, gibt es Möglichkeiten ... Ich werde bearbeiten, um eine andere Option anzuzeigen ...
Reed Copsey
1
@ Polaris878: Ich würde vorschlagen, Ihre Frage neu zu formulieren: "Warum sollte die Laufzeit eine Nullprüfung durchführen, bevor der Enumerator abgerufen wird?"
Reed Copsey
Ich frage wohl "warum nicht?" lol es scheint, als wäre das Verhalten immer noch gut definiert
Polaris878
2
@ Polaris878: Ich denke, die Art und Weise, wie ich das sehe, ist die Rückgabe von Null für eine Sammlung ein Fehler. So wie es jetzt ist, gibt Ihnen die Laufzeit in diesem Fall eine sinnvolle Ausnahme, aber es ist einfach, sie zu umgehen (dh oben), wenn Sie dieses Verhalten nicht mögen. Wenn der Compiler dies vor Ihnen verbirgt, verlieren Sie die Fehlerprüfung zur Laufzeit, aber es gibt keine Möglichkeit, sie auszuschalten ...
Reed Copsey
12

Es wird lange zurück beantwortet, aber ich habe versucht, dies auf die folgende Weise zu tun, um nur eine Nullzeigerausnahme zu vermeiden, und kann für jemanden nützlich sein, der den C # -Nullprüfungsoperator verwendet?

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });
Devesh
quelle
@kjbartel hat Sie um über ein Jahr geschlagen (unter " stackoverflow.com/a/32134295/401246 "). ;) Dies ist die beste Lösung, da sie nicht: a) eine Leistungsverschlechterung der (auch wenn nicht null) Verallgemeinerung der gesamten Schleife auf das LCD von Enumerable(wie bei Verwendung ??) beinhaltet, b) das Hinzufügen einer Erweiterungsmethode zu jedem Projekt erfordert; und c) zunächst das Vermeiden von null IEnumerables (Pffft! Puh-LEAZE! SMH. ) erfordern .
Tom
10

Eine weitere Erweiterungsmethode, um dies zu umgehen:

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

Auf verschiedene Arten konsumieren:

(1) mit einer Methode, die akzeptiert T:

returnArray.ForEach(Console.WriteLine);

(2) mit einem Ausdruck:

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

(3) mit einer mehrzeiligen anonymen Methode

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});
Jay
quelle
Im dritten Beispiel fehlt nur eine schließende Klammer. Ansonsten schöner Code, der auf interessante Weise weiter erweitert werden kann (für Schleifen, Umkehren, Springen usw.). Danke für das Teilen.
Lara
Vielen Dank für diesen wunderbaren Code. Aber ich habe die ersten Methoden nicht verstanden, warum Sie console.writeline als Parameter übergeben, obwohl es die Array-Elemente druckt. Aber nicht verstanden
Ajay Singh
@AjaySingh Console.WriteLineist nur ein Beispiel für eine Methode, die ein Argument (ein Action<T>) akzeptiert . Die Punkte 1, 2 und 3 zeigen Beispiele für die Übergabe von Funktionen an die .ForEachErweiterungsmethode.
Jay
@ kjbartel Antwort (bei „ stackoverflow.com/a/32134295/401246 “ ist die beste Lösung, weil es nicht der Fall ist: a) eine Leistungsverschlechterung von (auch wenn sie nicht beteiligt null) verallgemeinern die gesamte Schleife zum LCD Enumerable(wie die Verwendung ??würde ), b) erfordern, dass jedem Projekt eine Erweiterungsmethode hinzugefügt wird, oder c) dass zunächst null IEnumerables (Pffft! Puh-LEAZE! SMH.) vermieden wird (cuz nullbedeutet N / A, während leere Liste bedeutet, dass es anwendbar ist, aber ist Derzeit gut leer !, dh ein Mitarbeiter könnte Provisionen haben, die für Nicht-Verkäufe nicht zutreffend oder für Verkäufe leer sind.
Tom
5

Schreiben Sie einfach eine Erweiterungsmethode, um Ihnen zu helfen:

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);
      }
   }
}
BKostenlos
quelle
5

Weil eine Nullsammlung nicht dasselbe ist wie eine leere Sammlung. Eine leere Sammlung ist ein Sammlungsobjekt ohne Elemente. Eine Nullsammlung ist ein nicht vorhandenes Objekt.

Hier ist etwas zu versuchen: Deklarieren Sie zwei Sammlungen jeglicher Art. Initialisieren Sie eine normalerweise so, dass sie leer ist, und weisen Sie der anderen den Wert zu null. Versuchen Sie dann, beiden Sammlungen ein Objekt hinzuzufügen, und sehen Sie, was passiert.

JAB
quelle
3

Es ist die Schuld von Do.Something(). Die beste Vorgehensweise wäre hier, ein Array der Größe 0 (das ist möglich) anstelle einer Null zurückzugeben.

Henk Holterman
quelle
2

Denn hinter den Kulissen foreacherwirbt der einen Enumerator, der dem entspricht:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}
Lucero
quelle
2
so? Warum kann es nicht einfach überprüfen, ob es zuerst null ist, und die Schleife überspringen? AKA, was genau wird in den Erweiterungsmethoden gezeigt? Die Frage ist, ist es besser, standardmäßig die Schleife zu überspringen, wenn null, oder eine Ausnahme auszulösen? Ich denke es ist besser zu überspringen! Es scheint wahrscheinlich , dass null Behälter übersprungen werden sollen , anstatt geschlungen über da Schleifen , etwas tun sollen IF der Behälter nicht leer ist.
AbstractDissonance
@AbstractDissonance Sie könnten mit allen nullReferenzen dasselbe argumentieren , z. B. beim Zugriff auf Mitglieder. In der Regel ist dies ein Fehler, und wenn dies nicht der Fall ist, ist es einfach genug, dies beispielsweise mit der Erweiterungsmethode zu behandeln, die ein anderer Benutzer als Antwort angegeben hat.
Lucero
1
Das glaube ich nicht. Das foreach soll über die Sammlung operieren und unterscheidet sich von der direkten Referenzierung eines Nullobjekts. Man könnte zwar dasselbe argumentieren, aber ich wette, wenn Sie den gesamten Code der Welt analysieren würden, hätten die meisten foreach-Schleifen Nullprüfungen vor sich, nur um die Schleife zu umgehen, wenn die Sammlung "null" ist (was ist) daher genauso behandelt wie leer). Ich glaube nicht, dass irgendjemand das Schleifen über eine Nullsammlung als etwas betrachtet, das er möchte, und würde die Schleife lieber einfach ignorieren, wenn die Sammlung Null ist. Vielleicht könnte eher ein foreach? (Var x in C) verwendet werden.
AbstractDissonance
Der Punkt, den ich hauptsächlich versuche, ist, dass es ein bisschen Abfall im Code erzeugt, da man jedes Mal ohne guten Grund überprüfen muss. Die Erweiterungen funktionieren natürlich, aber eine Sprachfunktion könnte hinzugefügt werden, um diese Dinge ohne große Probleme zu vermeiden. (Hauptsächlich denke ich, dass die aktuelle Methode versteckte Fehler erzeugt, da der Programmierer möglicherweise vergisst, die Prüfung durchzuführen, und daher eine Ausnahme darstellt ... weil er entweder erwartet, dass die Prüfung an einer anderen Stelle vor der Schleife stattfindet, oder denkt, dass sie vorinitialisiert wurde (welche) es kann oder kann sich geändert haben). Aber in beiden
AbstractDissonance
@AbstractDissonance Nun, mit einer richtigen statischen Analyse wissen Sie, wo Sie Nullen haben könnten und wo nicht. Wenn Sie eine Null erhalten, bei der Sie keine erwarten, ist es besser, zu scheitern, anstatt Probleme IMHO stillschweigend zu ignorieren (im Sinne eines schnellen Scheiterns ). Daher halte ich dies für das richtige Verhalten.
Lucero
1

Ich denke, die Erklärung, warum eine Ausnahme ausgelöst wird, ist mit den hier gegebenen Antworten sehr klar. Ich möchte nur die Art und Weise ergänzen, wie ich normalerweise mit diesen Sammlungen arbeite. Weil ich die Sammlung manchmal mehrmals benutze und jedes Mal testen muss, ob sie null ist. Um dies zu vermeiden, mache ich Folgendes:

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

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

Auf diese Weise können wir die Sammlung so oft verwenden, wie wir möchten, ohne die Ausnahme zu befürchten, und wir verschmutzen den Code nicht mit übermäßigen bedingten Anweisungen.

Die Verwendung des Nullprüfungsoperators ?.ist ebenfalls ein guter Ansatz. Bei Arrays (wie im Beispiel in der Frage) sollte es jedoch vorher in List umgewandelt werden:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });
Alielson Piffer
quelle
2
Das Konvertieren in eine Liste, nur um Zugriff auf die ForEachMethode zu haben, ist eines der Dinge, die ich in einer Codebasis hasse.
Huysentruitw
Ich stimme zu ... ich vermeide das so weit wie möglich. :(
Alielson Piffer
-2
SPListItem item;
DataRow dr = datatable.NewRow();

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