2015-05-11 24 views
116

Ho un metodo di estensione stringa C# che dovrebbe restituire uno IEnumerable<int> di tutti gli indici di una sottostringa all'interno di una stringa. Funziona perfettamente per lo scopo previsto e vengono restituiti i risultati attesi (come dimostrato da uno dei miei test, anche se non quello riportato di seguito), ma un altro test di unità ha rilevato un problema con esso: non può gestire argomenti nulli.Perché questo metodo di estensione stringa non genera un'eccezione?

Ecco il metodo di estensione sto testando:

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; 
    } 
} 

Ecco il test che contrassegnato il problema:

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

Quando il test viene eseguito contro il mio metodo di estensione, non riesce, con il messaggio di errore standard che il metodo "non ha generato un'eccezione".

Questo è confusa: ho superato nettamente null nella funzione, ma per qualche motivo il confronto null == null sta tornando false. Pertanto, non viene generata alcuna eccezione e il codice continua.

mi hanno confermato questo non è un bug con il test: quando si esegue il metodo nel mio progetto principale con una chiamata al Console.WriteLine nel nulla-confronto if blocco, nulla è mostrato sulla console e non fa eccezione e 'colto da qualsiasi catch blocco che aggiungo. Inoltre, l'utilizzo di string.IsNullOrEmpty anziché lo == null ha lo stesso problema.

Perché questo confronto apparentemente semplice non riesce?

+5

Hai provato passando attraverso il codice? Probabilmente questo verrà risolto abbastanza rapidamente. –

+1

Cosa * succede * succede? (Esegue * un'eccezione *, in tal caso, quale riga e quale?) – user2864740

+0

@ user2864740 Ho descritto tutto ciò che accade. Nessuna eccezione, solo un test fallito e un metodo di esecuzione. – ArtOfCode

risposta

153

Si utilizza yield return. In tal caso, il compilatore riscriverà il metodo in una funzione che restituisce una classe generata che implementa una macchina a stati.

In linea generale, riscrive i locali nei campi di quella classe e ogni parte dell'algoritmo tra le istruzioni yield return diventa uno stato. È possibile verificare con un decompilatore che cosa questo metodo diventa dopo la compilazione (assicurarsi di disattivare la decompilazione intelligente che produrrebbe yield return).

Ma la riga di fondo è: il codice del metodo non verrà eseguito finché non si avvia l'iterazione.

Il solito modo per verificare la presenza di presupposti è quello di dividere il metodo in due:

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; 
    } 
} 

Questo funziona perché il primo metodo si comporterà esattamente come ci si aspetta (esecuzione immediata), e restituirà la macchina dello Stato implementato dal secondo metodo.

noti che si dovrebbe anche controllare il parametro str per null, perché i metodi estensioni possono essere chiamati null valori, sono solo zucchero sintattico.


Se siete curiosi di sapere che cosa il compilatore fa al vostro codice, qui è il tuo metodo, decompilato con dotPeek utilizzando il Mostra generato dal compilatore codice opzione.

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() 
    { 
    } 
} 

Questo è valido codice C#, perché il compilatore è permesso di fare le cose la lingua non consente, ma che sono legali in IL - per esempio la denominazione delle variabili in un modo che non si poteva evitare nome collisioni.

Tuttavia, come potete vedere, lo AllIndexesOf solo costruisce e restituisce un oggetto, il cui costruttore inizializza solo uno stato. GetEnumerator copia solo l'oggetto. Il vero lavoro viene eseguito quando si avvia l'enumerazione (chiamando il metodo MoveNext).

+9

BTW, ho aggiunto il seguente punto importante alla risposta: * Si noti che si dovrebbe anche controllare il parametro 'str' per' null', perché i metodi di estensioni possono essere chiamati su valori 'null', in quanto sono solo zucchero sintattico. * –

+2

'yield return 'è una buona idea in linea di principio, ma ha trucchi strani. Grazie per aver portato questo alla luce! – nateirvin

+0

Quindi, in pratica, un errore verrebbe generato se l'enumeratore fosse eseguito, come in un foreach? – MVCDS

34

Si dispone di un blocco iteratore. Nessuno del codice in quel metodo viene mai eseguito al di fuori delle chiamate a MoveNext sull'iteratore restituito. Chiamare il metodo non fa che creare la macchina a stati, e questo non fallirà mai (al di fuori di estremi come errori di memoria insufficienti, stack overflow, o eccezioni di interruzione del thread).

Quando si tenta effettivamente di ripetere la sequenza, si otterranno le eccezioni.

Questo è il motivo per cui i metodi LINQ hanno effettivamente bisogno di due metodi per avere l'errore che gestisce la semantica desiderata. Hanno un metodo privato che è un blocco iteratore e quindi un metodo di blocco non iteratore che non fa altro che eseguire la convalida dell'argomento (in modo che possa essere eseguito in modo avido, piuttosto che essere differito) mentre rimanda comunque tutte le altre funzionalità.

Quindi questo è il modello generale:

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

Gli enumeratori, come gli altri hanno detto, non vengono valutati fino al momento in cui iniziano a essere enumerati (cioè viene chiamato il metodo IEnumerable.GetNext). Così questo

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

non viene valutata fino a quando si inizia enumerando, vale a dire

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