2010-09-30 18 views
8

ho un mucchio di classi che possono Process() oggetti, e ritornano i propri oggetti:Wrap un IEnumerable e cattura eccezioni

public override IEnumerable<T> Process(IEnumerable<T> incoming) { ... } 

Voglio scrivere una classe processore in grado di avvolgere uno di questi processori, e registra eventuali eccezioni non rilevate che il metodo spostato Process() potrebbe generare. La mia prima idea era qualcosa di simile:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    try { 
     foreach (var x in this.processor.Process(incoming)) { 
      yield return x; 
     } 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 
} 

ma questo non funziona, a causa di CS1626: Cannot yield a value in the body of a try block with a catch clause.

Quindi voglio scrivere qualcosa che sia concettualmente equivalente ma compilato. :-) Ho questo:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    IEnumerator<T> walker; 
    try { 
     walker = this.processor.Process(incoming).GetEnumerator(); 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 

    while (true) { 
     T value; 
     try { 
      if (!walker.MoveNext()) { 
       break; 
      } 
      value = walker.Current; 
     } catch (Exception e) { 
      WriteToLog(e); 
      throw; 
     } 
     yield return value; 
    } 
} 

ma questo è più complessa di quanto avevo sperato, e io non sono del tutto certo di una sua correttezza o che non c'è un modo molto più semplice.

Sono sulla pista giusta qui? C'è un modo più semplice?

+1

Cosa ne pensi dell'implementazione di IEnumerable e delle eccezioni all'interno di MoveNext e Current? –

+0

@ Matt, penso che sia la risposta corretta. Avresti dovuto renderlo tale. Niente di male con breve e dolce. :) –

+0

Perché non basta rimuovere il ciclo foreach e cedere? –

risposta

-1

Il ciclo e la resa non sono necessari (almeno nell'esempio). Semplice rimuoverli e non c'è più una restrizione sui blocchi di cattura.

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    try { 
     return this.processor.Process(incoming); 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 
} 

Io parto dal presupposto this.processor.Process(incoming) restituisce un insieme attuazione IEnumerable<T>, in modo quindi non è necessario creare un nuovo iteratore. Se this.processor.Process(incoming) è pigro valutare poi

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    try { 
     return this.processor.Process(incoming).ToList(); 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 
} 
+0

dove dice 'this.processor.Process (incoming)' è pigro? –

+0

@Bear Monkey, riformulerò. Se l'eccezione si verifica dopo l'invocazione di 'GetEnumerator()', il tuo codice non lo intercetterà perché non invoca mai 'GetEnumerator()'. –

+0

@Kirk non hai senso. –

0

Potrebbe essere più bello se l'oggetto process era qualcosa che ha elaborato un solo elemento nella lista alla volta (se è ancora possibile), in questo modo si potrebbe raccogliere ogni singola eccezione e restituire un AggregateException proprio come fa la libreria Task Parallel.

[Se processo opera in lista nel suo complesso, o se una sola eccezione deve interrompere l'intero processo, ovviamente, questo suggerimento non è appropriato.]

+0

Potrebbe essere più bello, o potrebbe non, ma è un'interfaccia (semi-esterna) che non posso cambiare, almeno non in tempi ragionevoli. :-) – Ken

-1

Invece di risolvere solo il problema specifico, è può avere queste funzioni di aiuto nel vostro toolkit:

static IEnumerable<T> RunEnumerator<T>(Func<IEnumerator<T>> generator, 
     Action<Exception> onException) 
    { 
     using (var enumerator = generator()) 
     { 
      if (enumerator == null) 
       yield break; 
      for (; ;) 
      { 
       //Avoid creating a default value with 
       //unknown type T, as we don't know is it possible to do so 
       T[] value = null; 
       try 
       { 
        if (enumerator.MoveNext()) 
         value = new T[] { enumerator.Current }; 
       } 
       catch (Exception e) 
       { 
        onException(e); 
       } 
       if (value != null) 
        yield return value[0]; 
       else 
        yield break; 
      } 
     } 
    } 

    public static IEnumerable<T> WithExceptionHandler<T>(this IEnumerable<T> orig, 
     Action<Exception> onException) 
    { 
     return RunEnumerator(() => 
     { 
      try 
      { 
       return orig.GetEnumerator(); 
      } 
      catch (Exception e) 
      { 
       onException(e); 
       return null; 
      } 
     }, onException); 
    } 
} 

WithExceptionHandler<T> di convertire una IEnumerable<T> in un altro, e quando si verifica un'eccezione, il onException callback verrà chiamata. In questo modo si può avere la funzione di process implementata in questo modo:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    return incoming.WithExceptionHandler(e => { 
     WriteToLog(e); 
     throw; 
    } 
} 

In questo modo, si può anche fare qualcos'altro come ignorare l'eccezione completamente e lasciare che l'iterazione si ferma semplicemente. È anche possibile regolarlo un po 'utilizzando uno Func<Exception, bool> anziché Action<Exception> e consente alla callback di eccezione di decidere se l'iterazione continuerà o meno.

+0

Questo non funzionerà come previsto, se enumerator.MoveNext() genera un'eccezione, l'enumeratore è effettivamente morto, non si otterrà mai l'elemento successivo dall'enumeratore in questo modo. La tua iterazione si fermerà sempre alla prima eccezione, quindi il tuo wrapper non ha ottenuto nulla di più che se avessi avvolto il ciclo di elaborazione originale in un blocco catch try. –

+0

Bene, questo dipende da come viene implementato il tuo enumeratore. Puoi facilmente scrivere il tuo 'IEnumerator' che aggira questo limite. Sebbene non sia possibile fare affidamento su altri sistemi, almeno è possibile utilizzarlo per il proprio codice. –

+1

Non fraintendermi, sono veramente interessato a una soluzione come questa. Mi piacerebbe vedere un esempio che funziona :) Ma se MoveNext() genera un'eccezione è brusco, se MoveNext gestisce l'eccezione, quindi MoveNext restituirà true, quindi tutto questo wrappering non è assolutamente necessario. Questo è quello che sto ottenendo, questo wrapper non salterà mai l'eccezione, gestirà il primo ed uscirà con grazia, ma non arriverà mai al prossimo elemento dopo la prima eccezione. –

4

Un'estensione di linq può essere scritta per saltare tutti gli elementi che causano un'eccezione e passarla a una funzione di registrazione.

public static IEnumerable<T> CatchExceptions<T> (this IEnumerable<T> src, Action<Exception> action = null) { 
     using (var enumerator = src.GetEnumerator()) { 
      bool next = true; 

      while (next) { 
       try { 
        next = enumerator.MoveNext(); 
       } catch (Exception ex) { 
        if (action != null) { 
         action(ex); 
        } 
        continue; 
       } 

       if (next) { 
        yield return enumerator.Current; 
       } 
      } 
     } 
    } 

Esempio:

ienumerable.Select(e => e.something).CatchExceptions().ToArray() 

ienumerable.Select(e => e.something).CatchExceptions((ex) => Logger.Log(ex, "something failed")).ToArray() 
+1

Non funzionerà come previsto, probabilmente ho inserito questo thread con la spiegazione, ma ho tentato di testare accuratamente ciascuna delle soluzioni elencate. Non salterà l'oggetto eccezionale nell'iterazione, si fermerà alla prima eccezione. Dopo che l'enumeratore ha fallito, l'oggetto non può riprendere dalla successiva iterazione, non nel modo in cui è codificato qui. Quindi .ToArray() conterrà solo tutti gli elementi prima dell'eccezione. Tuttavia fallirà con garbo, quindi invece di CatchExceptions(), dovresti chiamarlo UntilException() :) –

3

Se ciò che si vuole fare è gestire un'eccezione durante l'elaborazione del risultato di un'enumerazione, quindi si tenta di logica ha bisogno semplicemente di andare direttamente all'interno del vostro per/while ciclo continuo.

Ma l'esempio si legge come se si stia tentando di rilevare e saltare le eccezioni generate dal provider di enumerazione.

Per quanto posso accertare, non vi è alcun modo in C# per iterare su un enumeratore e saltare ed eccezione che si verifica all'interno dell'enumeratore stesso. Se l'enumeratore solleva un'eccezione, tutte le future chiamate a MoveNext() generano un output falso.

Il modo più semplice per spiegare il motivo per cui questo accade è con questo molto semplice enumerabile:

IEnumerable<int> TestCases() 
{ 
    yield return 1; 
    yield return 2; 
    throw new ApplicationException("fail eunmeration"); 
    yield return 3; 
    yield return 4; 
} 

Comprensibilmente quando guardiamo questo esempio è evidente che l'eccezione generata causerà tutta questo blocco per uscire, e la dichiarazione di rendimento 3 e 4 non verrà mai elaborata. In effetti, si ottiene il solito avviso del compilatore "Unreachable code detected" sulla terza dichiarazione di rendimento.

Così, quando l'enumerabile è un più complesso, si applicano le stesse regole:

IEnumerable<int> TestCases2() 
{ 
    foreach (var item in Enumerable.Range(0,10)) 
    { 
     switch(item) 
     { 
      case 2: 
      case 5: 
       throw new ApplicationException("This bit failed"); 
      default: 
       yield return item; 
       break; 
     } 
    } 
} 

Quando l'eccezione è sollevata, l'elaborazione di questo blocco cessa e passa di nuovo la chiamata stack per il gestore di eccezioni più vicino.

TUTTI gli esempi utili per aggirare questo problema che ho trovato su SO non procedono all'elemento successivo nell'enumerazione, sono tutti interrompono alla prima eccezione.

Pertanto a skip it eccezione nell'enumerazione è necessario il provider per facilitarla. Questo è possibile solo se il vostro veramente codificato il provider, oppure è possibile contattare lo sviluppatore che ha fatto, il seguente è un esempio troppo semplificata di come si potrebbe raggiungere questo obiettivo:

IEnumerable<int> TestCases3(Action<int, Exception> exceptionHandler) 
{ 
    foreach (var item in Enumerable.Range(0, 10)) 
    { 
     int value = default(int); 
     try 
     { 
      switch (item) 
      { 
       case 2: 
       case 5: 
        throw new ApplicationException("This bit failed"); 
       default: 
        value = item; 
        break; 
      } 
     } 
     catch(Exception e) 
     { 
      if (exceptionHandler != null) 
      { 
       exceptionHandler(item, e); 
       continue; 
      } 
      else 
       throw; 
     } 
     yield return value; 
    } 
} 

...

foreach (var item in TestCases3(
    (int item, Exception ex) 
    => 
    Console.Out.WriteLine("Error on item: {0}, Exception: {1}", item, ex.Message))) 
{ 
    Console.Out.WriteLine(item); 
} 

questo produrrà il seguente output:

0 
1 
Error on item: 2, Exception: This bit failed 
3 
4 
Error on item: 5, Exception: This bit failed 
6 
7 
8 
9 

Spero che questo risolve il problema per altri sviluppatori in futuro in quanto è un'idea abbastanza comune che tutti noi una volta che iniziamo Getti ng in profondità in Linq ed enumerazioni. Roba potente ma ci sono alcune limitazioni logiche.

Problemi correlati