Ich habe eine C # -String-Erweiterungsmethode, die einen IEnumerable<int>
der Indizes eines Teilstrings innerhalb eines Strings zurückgeben soll. Es funktioniert perfekt für den beabsichtigten Zweck und die erwarteten Ergebnisse werden zurückgegeben (wie durch einen meiner Tests bewiesen, obwohl nicht der folgende), aber ein anderer Komponententest hat ein Problem damit entdeckt: Es kann keine Nullargumente verarbeiten.
Hier ist die Erweiterungsmethode, die ich teste:
public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
if (searchText == null)
{
throw new ArgumentNullException("searchText");
}
for (int index = 0; ; index += searchText.Length)
{
index = str.IndexOf(searchText, index);
if (index == -1)
break;
yield return index;
}
}
Hier ist der Test, der das Problem gemeldet hat:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
Wenn der Test für meine Erweiterungsmethode ausgeführt wird, schlägt er mit der Standardfehlermeldung fehl, dass die Methode "keine Ausnahme ausgelöst hat".
Das ist verwirrend: Ich bin eindeutig null
auf die Funktion übergegangen , aber aus irgendeinem Grund null == null
kehrt der Vergleich zurückfalse
. Daher wird keine Ausnahme ausgelöst und der Code wird fortgesetzt.
Ich habe bestätigt, dass dies kein Fehler beim Test ist: Wenn Sie die Methode in meinem Hauptprojekt mit einem Aufruf von Console.WriteLine
im Nullvergleichsblock if
ausführen, wird auf der Konsole nichts angezeigt und keine Ausnahme wird von einem von catch
mir hinzugefügten Block abgefangen . Darüber hinaus hat die Verwendung von string.IsNullOrEmpty
anstelle von == null
das gleiche Problem.
Warum scheitert dieser vermeintlich einfache Vergleich?
quelle
Antworten:
Sie verwenden
yield return
. Dabei schreibt der Compiler Ihre Methode in eine Funktion um, die eine generierte Klasse zurückgibt, die eine Zustandsmaschine implementiert.Im Allgemeinen werden die Einheimischen in Felder dieser Klasse umgeschrieben, und jeder Teil Ihres Algorithmus zwischen den
yield return
Anweisungen wird zu einem Status. Sie können mit einem Dekompiler überprüfen, wie diese Methode nach der Kompilierung aussehen wird (stellen Sie sicher, dass die intelligente Dekompilierung deaktiviert ist, die dazu führen würdeyield return
).Das Fazit lautet jedoch: Der Code Ihrer Methode wird erst ausgeführt, wenn Sie mit der Iteration beginnen.
Die übliche Methode, um nach Voraussetzungen zu suchen, besteht darin, Ihre Methode in zwei Teile zu teilen:
Dies funktioniert, weil sich die erste Methode wie erwartet verhält (sofortige Ausführung) und die von der zweiten Methode implementierte Zustandsmaschine zurückgibt.
Beachten Sie, dass Sie auch die überprüfen sollten ,
str
Parameter fürnull
, da Erweiterungen Methoden können aufgefordert werden ,null
Werte, da sie nur syntaktischer Zucker sind.Wenn Sie neugierig sind, was der Compiler mit Ihrem Code macht, finden Sie hier Ihre Methode, die mit dotPeek mithilfe der Option Vom Compiler generierten Code anzeigen dekompiliert wurde .
Dies ist ein ungültiger C # -Code, da der Compiler Dinge tun darf, die die Sprache nicht zulässt, die aber in IL legal sind - zum Beispiel die Variablen so zu benennen, dass Namenskollisionen nicht vermieden werden können.
Wie Sie jedoch sehen können, erstellt
AllIndexesOf
und gibt der einzige ein Objekt zurück, dessen Konstruktor nur einen bestimmten Status initialisiert.GetEnumerator
kopiert nur das Objekt. Die eigentliche Arbeit ist erledigt, wenn Sie mit der Aufzählung beginnen (durch Aufrufen derMoveNext
Methode).quelle
str
Parameter überprüfen solltennull
, da Erweiterungsmethoden fürnull
Werte aufgerufen werden können, da es sich nur um syntaktischen Zucker handelt.yield return
ist im Prinzip eine schöne Idee, aber es gibt so viele seltsame Fallstricke. Danke, dass du dieses ans Licht gebracht hast!MoveNext
wird vomforeach
Konstrukt unter der Haube genannt . Ichforeach
habe in meiner Antwort eine Erklärung darüber geschrieben, was die Semantik der Sammlung erklärt, wenn Sie das genaue Muster sehen möchten.Sie haben einen Iteratorblock. Keiner der Codes in dieser Methode wird jemals außerhalb von Aufrufen von ausgeführt
MoveNext
des zurückgegebenen Iterators ausgeführt. Wenn Sie die Methode aufrufen, wird die Zustandsmaschine zwar notiert, aber erstellt, und dies wird niemals fehlschlagen (außerhalb von Extremen wie Speicherfehlern, Stapelüberläufen oder Thread-Abbruch-Ausnahmen).Wenn Sie tatsächlich versuchen, die Sequenz zu wiederholen, erhalten Sie die Ausnahmen.
Aus diesem Grund benötigen die LINQ-Methoden tatsächlich zwei Methoden, um die gewünschte Semantik für die Fehlerbehandlung zu erhalten. Sie haben eine private Methode, die ein Iteratorblock ist, und dann eine Nicht-Iteratorblockmethode, die nichts anderes tut, als die Argumentvalidierung durchzuführen (damit sie eifrig durchgeführt werden kann, anstatt sie zu verschieben), während alle anderen Funktionen immer noch zurückgestellt werden.
Das ist also das allgemeine Muster:
quelle
Enumeratoren werden, wie die anderen gesagt haben, erst ausgewertet, wenn sie mit der Aufzählung beginnen (dh die
IEnumerable.GetNext
Methode wird aufgerufen). Also das hierwird erst ausgewertet, wenn Sie mit der Aufzählung beginnen, d. h
quelle