2010-09-30 3 views
8

J'ai un tas de classes qui peuvent Process() objets et retourner leurs propres objets:enveloppe un IEnumerable et capture des exceptions

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

Je veux écrire une classe de processeur qui peut envelopper un de ces processeurs, et consignez toutes les exceptions non interceptées que la méthode Process() enveloppée peut lancer. Ma première idée était quelque chose comme ceci:

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

mais cela ne fonctionne pas, en raison de CS1626: Cannot yield a value in the body of a try block with a catch clause. Donc je veux écrire quelque chose qui est conceptuellement équivalent mais compile. :-) J'ai ceci:

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

mais c'est plus complexe que je ne l'avais espéré, et je ne suis pas tout à fait certain soit son exactitude ou qu'il n'y a pas une façon beaucoup plus simple.

Suis-je sur la bonne voie ici? Y a-t-il un moyen plus facile?

+1

Qu'en est-il de l'implémentation directe de IEnumerable et de la détection d'exceptions dans votre MoveNext et Current? –

+0

@Matt, je pense que c'est la bonne réponse. Vous devriez l'avoir fait tel. Rien de mal à court et doux. :) –

+0

Pourquoi ne pas simplement enlever la boucle foreach et céder? –

Répondre

-1

La boucle et le rendement ne sont pas nécessaires (au moins dans votre exemple). Simple les supprimer et il n'y a plus de restriction sur les blocs de capture.

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

Je suppose this.processor.Process(incoming) retourne une collection mise en œuvre IEnumerable<T>, donc par conséquent, vous n'avez pas besoin de créer une nouvelle iterator. Si this.processor.Process(incoming) est paresseux évaluer alors

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

où dit-on 'this.processor.Process (incoming)' est paresseux? –

+0

@Bear Monkey, je vais reformuler. Si l'exception se produit lors de l'appel de 'GetEnumerator()', votre code ne l'attrape pas car il n'invoque jamais 'GetEnumerator()'. –

+0

@Kirk vous n'avez aucun sens. –

0

Il est peut-être plus agréable si l'objet process était quelque chose qui a traité un seul élément dans la liste à la fois (si cela est encore possible), de cette façon vous pouvez recueillir chaque exception et retourne un AggregateException exactement comme le fait la librairie Task Parallel.

[si le processus fonctionne sur la liste dans son ensemble, ou si une seule exception a besoin d'interrompre le processus évidemment cette suggestion ne convient pas.]

+0

Il pourrait être plus agréable, ou peut-être pas, mais c'est une interface (semi-externe) je ne peux pas changer, au moins pas sur une échelle de temps raisonnable. :-) – Ken

-1

Au lieu de résoudre votre problème uniquement, vous peut avoir ces fonctions d'aide dans votre boîte à outils:

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> convertit un IEnumerable<T> dans un autre, et quand exception se produit, le rappel onException sera appelée. De cette façon, vous pouvez avoir votre fonction process mis en œuvre comme ceci:

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

De cette façon, vous pouvez aussi faire quelque chose d'autre comme ignorer l'exception complètement et laissez l'itération s'arrête simplement. Vous pouvez également l'ajuster un peu en utilisant un Func<Exception, bool> au lieu de Action<Exception> et permet au rappel d'exception de décider si l'itération doit continuer ou non.

+0

Cela ne fonctionnera pas comme prévu, si enumerator.MoveNext() lève une exception, l'énumérateur est effectivement mort, vous n'obtiendrez jamais à l'élément suivant de l'énumérateur de cette manière. Votre itération s'arrête toujours à la première exception, donc votre wrapper n'a rien obtenu de plus que si vous aviez enveloppé la boucle de traitement d'origine dans un bloc try catch. –

+0

Eh bien, cela dépend de la façon dont votre énumérateur est implémenté. Vous pouvez facilement écrire votre propre 'IEnumerator' qui contourne cette limite. Bien que vous ne puissiez pas compter sur d'autres systèmes, vous pouvez au moins utiliser votre propre code. –

+1

Ne vous méprenez pas, je suis vraiment intéressé par une solution comme celle-ci. J'aimerais voir un exemple qui fonctionne :) Mais si MoveNext() lance une exception c'est toast, si MoveNext gère l'exception, alors MoveNext retournera vrai, donc tout ce wrappering est totalement inutile. C'est ce que je veux dire, ce wrapper n'interrompra jamais l'exception, il gérera le premier et quittera gracieusement, mais il n'atteindra jamais l'élément suivant après la première exception. –

4

Une extension linq peut être écrite pour ignorer tous les éléments provoquant une exception et la transmettre à une fonction de journalisation.

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

Exemple:

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

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

Cela ne fonctionnera pas comme prévu, j'ai probablement jeté ce fil avec l'explication, mais j'ai essayé de tester soigneusement chacune des solutions énumérées. Il ne sautera pas l'élément exceptionnel dans l'itération, il s'arrêtera à la première exception. Après l'échec de l'énumérateur, l'objet ne peut pas reprendre à partir de l'itération suivante, pas de la façon dont il est codé ici. Donc, votre .ToArray() ne contiendra jamais tous les éléments avant l'exception. Il échouera gracieusement cependant, donc au lieu de CatchExceptions(), vous devriez l'appeler UntilException() :) –

3

Si ce que vous voulez faire est de gérer une exception lors du traitement du résultat d'une énumération, vous essayez la logique a simplement besoin d'aller directement à l'intérieur de votre pour/while boucle. Mais votre exemple se lit comme si vous essayez d'intercepter et de passer outre les exceptions déclenchées par le fournisseur d'énumération. Pour autant que je sache, il n'y a aucun moyen en C# d'itérer sur un énumérateur et un saut et une exception qui se produit dans l'énumérateur lui-même. Si l'énumérateur déclenche une exception, tous les futurs appels à MoveNext() entraîneront une sortie erronée.

La meilleure façon d'expliquer pourquoi cela se produit est avec ce très simples dénombrable:

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

Naturellement quand on regarde cet exemple, il est évident que l'exception jetée provoquera ce bloc entier pour quitter, et les troisième et quatrième relevés de rendement ne seront jamais traités. En fait, l'avertissement habituel du compilateur 'Unreachable code detected' s'affiche sur la 3ème déclaration de rendement.

Ainsi, lorsque le dénombrable est un plus complexe, les mêmes règles sont applicables:

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

Lorsque l'exception est soulevée, le traitement de ce bloc cesse et passe en arrière la pile d'appel au gestionnaire d'exception le plus proche. Tous les exemples réalisables pour contourner ce problème que j'ai trouvé sur SO ne passent pas à l'élément suivant dans l'énumération, ils tous casse à la première exception.

Par conséquent ignorer en exception dans l'énumération vous aurez besoin du fournisseur pour le faciliter. Ceci est possible vraiment si votre fournisseur codé, ou vous pouvez contacter le développeur qui a fait, ce qui suit est un exemple succint de la façon dont vous pourriez y parvenir:

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

cela produira la sortie suivante:

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

J'espère que cela éclaircit la question pour d'autres développeurs à l'avenir car il est une idée assez commune que nous recevons tous une fois que nous commençons Getti profondément dans Linq et énumérations. Des trucs puissants mais il y a des limites logiques.

Questions connexes