2015-05-11 13 views
116

Ich habe eine C# -String-Erweiterung-Methode, die eine IEnumerable<int> aller Indizes einer Teilzeichenfolge innerhalb einer Zeichenfolge zurückgeben sollte. Es funktioniert perfekt für den beabsichtigten Zweck und die erwarteten Ergebnisse werden zurückgegeben (wie durch einen meiner Tests bewiesen, obwohl nicht der eine unten), aber ein anderer Komponententest hat ein Problem damit entdeckt: Es kann keine Nullargumente behandeln.Warum löst diese Zeichenfolgenerweiterungsmethode keine Ausnahme aus?

Hier ist die Erweiterung Methode, die ich testen bin:

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 gekennzeichnet up:

[TestMethod] 
[ExpectedException(typeof(ArgumentNullException))] 
public void Extensions_AllIndexesOf_HandlesNullArguments() 
{ 
    string test = "a.b.c.d.e"; 
    test.AllIndexesOf(null); 
} 

Wenn der Test gegen meine Erweiterungsmethode läuft, schlägt es mit die Standardfehlermeldung, dass die Methode "keine Ausnahme ausgelöst hat".

Das ist verwirrend: Ich habe klar null in die Funktion übergeben, aber aus irgendeinem Grund gibt der Vergleich null == nullfalse zurück. Daher wird keine Ausnahme ausgelöst und der Code wird fortgesetzt.

Ich habe bestätigt, dass es nicht um einen Fehler mit dem Test ist: wenn mit einem Aufruf an Console.WriteLine im Null Vergleich if Block die Methode in meinem Hauptprojekt läuft, wird von keinem nichts gefangen wird auf der Konsole und keine Ausnahme dargestellt catch Block ich füge hinzu. Außerdem hat die Verwendung von string.IsNullOrEmpty anstelle von == null das gleiche Problem.

Warum schlägt dieser vermeintlich einfache Vergleich fehl?

+5

Haben Sie versucht, durch den Code treten? Das wird es wahrscheinlich ziemlich schnell lösen. –

+1

Was * passiert *? (Es wirft * eine * Ausnahme; wenn ja, welche und welche Zeile?) – user2864740

+0

@ user2864740 Ich habe alles beschrieben, was passiert. Keine Ausnahmen, nur ein fehlgeschlagener Test und eine Laufmethode. – ArtOfCode

Antwort

153

Sie yield return verwenden. Wenn Sie dies tun, überschreibt der Compiler Ihre Methode in eine Funktion, die eine generierte Klasse zurückgibt, die eine Zustandsmaschine implementiert.

Allgemein gesagt schreibt es Ortsansätze zu Feldern dieser Klasse um und jeder Teil Ihres Algorithmus zwischen den yield return Anweisungen wird zu einem Zustand. Sie können mit einem Decompiler überprüfen, was diese Methode nach der Kompilierung wird (stellen Sie sicher, dass Sie die intelligente Dekompilierung deaktivieren, die yield return erzeugt).

Aber die Quintessenz ist: der Code Ihrer Methode wird nicht ausgeführt, bis Sie Iteration beginnen.

Der üblicher Weg für Voraussetzungen zu überprüfen, ist Ihre Methode in zwei aufzuspalten:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText) 
{ 
    if (str == null) 
     throw new ArgumentNullException("str"); 
    if (searchText == null) 
     throw new ArgumentNullException("searchText"); 

    return AllIndexesOfCore(str, searchText); 
} 

private static IEnumerable<int> AllIndexesOfCore(string str, string searchText) 
{ 
    for (int index = 0; ; index += searchText.Length) 
    { 
     index = str.IndexOf(searchText, index); 
     if (index == -1) 
      break; 
     yield return index; 
    } 
} 

Das funktioniert, weil die erste Methode wie Sie erwarten (sofortige Ausführung) verhalten wird, und die Zustandsmaschine zurückkehrt implementiert mit der zweiten Methode.

Beachten Sie, dass Sie auch die str Parameter für null überprüfen sollten, da Erweiterungen Methoden können auf null Werte genannt werden, da sie nur syntaktischer Zucker sind.


Wenn Sie neugierig sind, was der Compiler den Code der Fall ist, hier ist Ihre Methode, dekompilierten mit dotPeek dem anzeigen Compiler generierten Code Option.

public static IEnumerable<int> AllIndexesOf(this string str, string searchText) 
{ 
    Test.<AllIndexesOf>d__0 allIndexesOfD0 = new Test.<AllIndexesOf>d__0(-2); 
    allIndexesOfD0.<>3__str = str; 
    allIndexesOfD0.<>3__searchText = searchText; 
    return (IEnumerable<int>) allIndexesOfD0; 
} 

[CompilerGenerated] 
private sealed class <AllIndexesOf>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable 
{ 
    private int <>2__current; 
    private int <>1__state; 
    private int <>l__initialThreadId; 
    public string str; 
    public string <>3__str; 
    public string searchText; 
    public string <>3__searchText; 
    public int <index>5__1; 

    int IEnumerator<int>.Current 
    { 
    [DebuggerHidden] get 
    { 
     return this.<>2__current; 
    } 
    } 

    object IEnumerator.Current 
    { 
    [DebuggerHidden] get 
    { 
     return (object) this.<>2__current; 
    } 
    } 

    [DebuggerHidden] 
    public <AllIndexesOf>d__0(int <>1__state) 
    { 
    base..ctor(); 
    this.<>1__state = param0; 
    this.<>l__initialThreadId = Environment.CurrentManagedThreadId; 
    } 

    [DebuggerHidden] 
    IEnumerator<int> IEnumerable<int>.GetEnumerator() 
    { 
    Test.<AllIndexesOf>d__0 allIndexesOfD0; 
    if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2) 
    { 
     this.<>1__state = 0; 
     allIndexesOfD0 = this; 
    } 
    else 
     allIndexesOfD0 = new Test.<AllIndexesOf>d__0(0); 
    allIndexesOfD0.str = this.<>3__str; 
    allIndexesOfD0.searchText = this.<>3__searchText; 
    return (IEnumerator<int>) allIndexesOfD0; 
    } 

    [DebuggerHidden] 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
    return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator(); 
    } 

    bool IEnumerator.MoveNext() 
    { 
    switch (this.<>1__state) 
    { 
     case 0: 
     this.<>1__state = -1; 
     if (this.searchText == null) 
      throw new ArgumentNullException("searchText"); 
     this.<index>5__1 = 0; 
     break; 
     case 1: 
     this.<>1__state = -1; 
     this.<index>5__1 += this.searchText.Length; 
     break; 
     default: 
     return false; 
    } 
    this.<index>5__1 = this.str.IndexOf(this.searchText, this.<index>5__1); 
    if (this.<index>5__1 != -1) 
    { 
     this.<>2__current = this.<index>5__1; 
     this.<>1__state = 1; 
     return true; 
    } 
    goto default; 
    } 

    [DebuggerHidden] 
    void IEnumerator.Reset() 
    { 
    throw new NotSupportedException(); 
    } 

    void IDisposable.Dispose() 
    { 
    } 
} 

Dies ist ungültig C# -Code, weil die Compiler Dinge zu tun erlaubt ist, die Sprache nicht erlaubt, die aber in IL legal ist - zum Beispiel der Variablen in einer Art und Weise zu benennen könnten Sie nicht Namen zu vermeiden Kollisionen.

Aber wie Sie sehen können, das AllIndexesOf Konstrukte und gibt nur ein Objekt zurück, dessen Konstruktor nur einige Zustände initialisiert. GetEnumerator kopiert nur das Objekt. Die eigentliche Arbeit ist erledigt, wenn Sie mit dem Aufzählen beginnen (indem Sie die Methode MoveNext aufrufen).

+9

BTW, fügte ich der Antwort folgenden wichtigen Punkt hinzu: * Beachten Sie, dass Sie auch den 'str' -Parameter für' null' überprüfen sollten, da Erweiterungsmethoden für 'null'-Werte aufgerufen werden können, da sie nur syntaktischer Zucker sind. * –

+2

'yield return' ist im Prinzip eine nette Idee, aber es hat so viele seltsame Probleme. Danke, dass du dieses Licht ans Licht gebracht hast! – nateirvin

+0

Also, im Grunde würde ein Fehler geworfen werden, wenn der Enumerator ausgeführt wurde, wie in einer foreach? – MVCDS

34

Sie haben einen Iteratorblock. Kein Code in dieser Methode wird jemals außerhalb von Aufrufen von MoveNext auf dem zurückgegebenen Iterator ausgeführt. Beim Aufrufen der Methode wird die Statusmaschine erstellt, die jedoch nie fehlschlägt (außerhalb von Extremfällen wie fehlendem Speicher, Stack-Überläufen oder Thread-Abbruch-Ausnahmen).

Wenn Sie tatsächlich versuchen, die Sequenz zu iterieren, erhalten Sie die Ausnahmen.

Aus diesem Grund benötigen die LINQ-Methoden tatsächlich zwei Methoden, um die gewünschte Semantik zur Fehlerbehandlung zu erhalten. Sie haben eine private Methode, die ein Iteratorblock ist, und dann eine Nicht-Iterator-Blockmethode, die nur die Argumentvalidierung durchführt (so dass sie eifrig ausgeführt werden kann, anstatt dass sie zurückgestellt wird), während alle anderen Funktionen verschoben werden.

Das ist also das allgemeine Muster:

public static IEnumerable<T> Foo<T>(
    this IEnumerable<T> souce, Func<T, bool> anotherArgument) 
{ 
    //note, not an iterator block 
    if(anotherArgument == null) 
    { 
     //TODO make a fuss 
    } 
    return FooImpl(source, anotherArgument); 
} 

private static IEnumerable<T> FooImpl<T>(
    IEnumerable<T> souce, Func<T, bool> anotherArgument) 
{ 
    //TODO actual implementation as an iterator block 
    yield break; 
} 
0

Enumeratoren, wie die anderen gesagt haben, werden nicht ausgewertet bis zu dem Zeitpunkt, zu dem sie gezählt werden (d. H. Die IEnumerable.GetNext-Methode wird aufgerufen). So ist diese

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>(); 

nicht ausgewertet wird, bis Sie Aufzählen beginnen, das heißt

foreach(int index in indexes) 
{ 
    // ArgumentNullException 
}