2009-08-13 7 views
119

J'ai la fonction suivante pour obtenir des erreurs de validation pour une carte. Ma question concerne le traitement de GetErrors. Les deux méthodes ont le même type de retour IEnumerable<ErrorInfo>.Retour de rendement imbriqué avec IEnumerable

private static IEnumerable<ErrorInfo> GetErrors(Card card) 
{ 
    var errors = GetMoreErrors(card); 
    foreach (var e in errors) 
     yield return e; 

    // further yield returns for more validation errors 
} 

Est-il possible de retourner toutes les erreurs GetMoreErrors sans avoir à énumérer eux? En pensant à cela, c'est probablement une question stupide, mais je veux m'assurer que je ne me trompe pas.

+0

Je suis heureux (et curieux!) De voir plus de questions sur le rendement des rendements - je ne comprends pas tout à fait moi-même. Pas une question stupide! – JoshJordan

+0

Qu'est-ce que 'GetCardProductionValidationErrorsFor'? –

+4

ce qui ne va pas avec * return GetMoreErrors (card); *? –

Répondre

107

Ce n'est certainement pas une question stupide, et c'est quelque chose que F # supporte avec yield! pour une collection entière par rapport à yield pour un seul article. (Cela peut être très utile en termes de récursion de queue ...)

Malheureusement, ce n'est pas supporté en C#.

Cependant, si vous avez plusieurs méthodes chaque retour d'un IEnumerable<ErrorInfo>, vous pouvez utiliser Enumerable.Concat pour rendre votre code plus simple:

private static IEnumerable<ErrorInfo> GetErrors(Card card) 
{ 
    return GetMoreErrors(card).Concat(GetOtherErrors()) 
           .Concat(GetValidationErrors()) 
           .Concat(AnyMoreErrors()) 
           .Concat(ICantBelieveHowManyErrorsYouHave()); 
} 

Il y a une différence très importante entre les deux implémentations cependant: celui-ci appellera tous les méthodes immédiatement, même si elle utilisera seulement les itérateurs retournés un à la fois. Votre code existant attendra jusqu'à ce qu'il soit bouclé à travers tout dans GetMoreErrors() avant même demande sur les prochaines erreurs.

Habituellement, ce n'est pas important, mais cela vaut la peine de comprendre ce qui se passera quand.

+2

Wes Dyer a un article intéressant mentionnant ce modèle. http://blogs.msdn.com/wesdyer/archive/2007/03/23/all-about-iterators.aspx – JohannesH

+1

Correction mineure pour les passants - System.Linq.Enumeration.Concat <> (premier, deuxième) . Pas IEnumeration.Concat(). – redcalx

+0

@ the-locster: Je ne suis pas sûr de ce que vous voulez dire. C'est définitivement Enumerable plutôt que Enumeration. Pourriez-vous clarifier votre commentaire? –

6

Je ne vois rien de mal avec votre fonction, je dirais qu'il fait ce que vous voulez. Pensez au rendement comme renvoyant un élément dans l'énumération finale chaque fois qu'il est invoqué, donc quand vous l'avez dans la boucle foreach comme ça, chaque fois qu'il est invoqué il renvoie 1 élément. Vous avez la possibilité de mettre des instructions conditionnelles dans votre foreach pour filtrer le resultset. (Simplement en ne cédant pas sur vos critères d'exclusion)

Si vous ajoutez des rendements ultérieurs plus tard dans la méthode, il continuera d'ajouter 1 élément à l'énumération, qui permet de faire des choses comme ...

public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists) 
{ 
    foreach (IEnumerable<string> list in lists) 
    { 
    foreach (string s in list) 
    { 
     yield return s; 
    } 
    } 
} 
5

Oui, il est possible de retourner toutes les erreurs en même temps. Renvoyez juste un List<T> ou ReadOnlyCollection<T>.

En renvoyant un IEnumerable<T>, vous renvoyez une séquence de quelque chose. Sur la surface qui peut sembler identique à retourner la collection, mais il y a un certain nombre de différences, vous devriez garder à l'esprit.

Collections

  • L'appelant peut être sûr que la collecte et tous les éléments existeront lorsque la collection est retourné. Si la collection doit être créée par appel, retourner une collection est une très mauvaise idée.
  • La plupart des collections peuvent être modifiées lorsqu'elles sont renvoyées.
  • La collection est de taille finie.

Sequences

  • Peut-être dénombrées - et qui est à peu près tout ce que nous pouvons dire à coup sûr.
  • Une séquence renvoyée elle-même ne peut pas être modifiée.
  • Chaque élément peut être créé dans le cadre de l'exécution de la séquence (c'est-à-dire que renvoyer IEnumerable<T> permet une évaluation paresseuse, ce qui n'est pas le cas pour List<T>).
  • Une séquence peut être infinie et laisser ainsi à l'appelant le soin de décider du nombre d'éléments à renvoyer.
+0

Renvoyer une collection peut entraîner un surcoût déraisonnable si tout le client a vraiment besoin d'être énuméré à travers, puisque vous allouez les structures de données pour tous les éléments à l'avance. En outre, si vous déléguez à une autre méthode renvoyant une séquence, la capture en tant que collection implique une copie supplémentaire et vous ne savez pas combien d'éléments (et donc combien de temps) cela peut impliquer. Ainsi, il est seulement une bonne idée de retourner la collection quand elle est déjà là et peut être retournée directement sans copier (ou emballée en lecture seule). Dans tous les autres cas, la séquence est un meilleur choix –

+0

Je suis d'accord, et si vous avez l'impression que j'ai dit retourner une collection est toujours une bonne idée que vous avez manqué mon point. J'essayais de mettre en évidence le fait qu'il y a des différences entre retourner une collection et renvoyer une séquence. Je vais essayer de le rendre plus clair. –

14

Vous pouvez configurer toutes les sources d'erreur comme ceci (noms de méthodes empruntés à la réponse de Jon Skeet).

private static IEnumerable<IEnumerable<ErrorInfo>> GetErrorSources(Card card) 
{ 
    yield return GetMoreErrors(card); 
    yield return GetOtherErrors(); 
    yield return GetValidationErrors(); 
    yield return AnyMoreErrors(); 
    yield return ICantBelieveHowManyErrorsYouHave(); 
} 

Vous pouvez ensuite itérer sur eux en même temps.

private static IEnumerable<ErrorInfo> GetErrors(Card card) 
{ 
    foreach (var errorSource in GetErrorSources(card)) 
     foreach (var error in errorSource) 
      yield return error; 
} 

Sinon, vous pouvez aplatir les sources d'erreur avec SelectMany.

private static IEnumerable<ErrorInfo> GetErrors(Card card) 
{ 
    return GetErrorSources(card).SelectMany(e => e); 
} 

L'exécution des méthodes GetErrorSources sera retardée aussi.

9

je suis venu avec un extrait yield_ rapide:

yield_ snipped usage animation

est ici l'extrait de code XML:

<?xml version="1.0" encoding="utf-8"?> 
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> 
    <CodeSnippet Format="1.0.0"> 
    <Header> 
     <Author>John Gietzen</Author> 
     <Description>yield! expansion for C#</Description> 
     <Shortcut>yield_</Shortcut> 
     <Title>Yield All</Title> 
     <SnippetTypes> 
     <SnippetType>Expansion</SnippetType> 
     </SnippetTypes> 
    </Header> 
    <Snippet> 
     <Declarations> 
     <Literal Editable="true"> 
      <Default>items</Default> 
      <ID>items</ID> 
     </Literal> 
     <Literal Editable="true"> 
      <Default>i</Default> 
      <ID>i</ID> 
     </Literal> 
     </Declarations> 
     <Code Language="CSharp"><![CDATA[foreach (var $i$ in $items$) yield return $i$$end$;]]></Code> 
    </Snippet> 
    </CodeSnippet> 
</CodeSnippets> 
1

Je ne suis pas surpris personne n'a pensé à recommander une méthode simple d'extension sur IEnumerable<IEnumerable<T>> pour que ce code conserve son exécution différée. Je suis un fan de l'exécution différée pour de nombreuses raisons, l'un d'eux est que l'empreinte mémoire est petite, même pour les énumérables énormes.

public static class EnumearbleExtensions 
{ 
    public static IEnumerable<T> UnWrap<T>(this IEnumerable<IEnumerable<T>> list) 
    { 
     foreach(var innerList in list) 
     { 
      foreach(T item in innerList) 
      { 
       yield return item; 
      } 
     } 
    } 
} 

Et vous pouvez l'utiliser dans votre cas comme celui-ci

private static IEnumerable<ErrorInfo> GetErrors(Card card) 
{ 
    return DoGetErrors(card).UnWrap(); 
} 

private static IEnumerable<IEnumerable<ErrorInfo>> DoGetErrors(Card card) 
{ 
    yield return GetMoreErrors(card); 

    // further yield returns for more validation errors 
} 

De même, vous pouvez en finir avec la fonction enveloppe autour DoGetErrors et il suffit de déplacer UnWrap au callsite.

+0

Probablement personne n'a pensé à une méthode d'extension car 'DoGetErrors (card) .SelectMany (x => x)' fait la même chose et préserve le comportement différé. Ce qui est exactement ce que Adam suggère dans [sa réponse] (http://stackoverflow.com/a/22912410/1300910). –

Questions connexes