web-dev-qa-db-fra.com

Retour de rendement imbriqué avec IEnumerable

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>.

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 renvoyer toutes les erreurs dans GetMoreErrors sans avoir à les énumérer?

En y réfléchissant, c'est probablement une question stupide, mais je veux m'assurer que je ne me trompe pas.

154
John Oxley

Ce n'est certainement pas une question stupide, et c'est quelque chose que F # prend en charge avec yield! Pour une collection entière vs yield pour un seul élément. (Cela peut être très utile en termes de récursivité de queue ...)

Malheureusement, il n'est pas pris en charge en C #.

Cependant, si vous disposez de plusieurs méthodes renvoyant chacune un IEnumerable<ErrorInfo>, Vous pouvez utiliser Enumerable.Concat Pour simplifier votre code:

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

Il existe cependant une différence très importante entre les deux implémentations: celle-ci appellera toutes les méthodes immédiatement, même si elle n'utilisera que les itérateurs retournés un par un. Votre code existant attendra 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.

134
Jon Skeet

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

Vous pouvez également 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 dans GetErrorSources sera également retardée.

21
Adam Boddington

Je suis venu avec un rapide yield_ extrait:

yield_ snipped usage animation

Voici l'extrait 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>
15
John Gietzen

Je ne vois rien de mal à votre fonction, je dirais qu'elle fait ce que vous voulez.

Considérez le 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 placer des instructions conditionnelles dans votre foreach pour filtrer l'ensemble de résultats. (simplement en ne cédant pas à 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, ce 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;
    }
  }
}
8
Tim Jarvis

Je suis surpris que personne n'ait pensé à recommander une méthode d'extension simple 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'une d'entre elles est que l'empreinte mémoire est petite, même pour les énumérations é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 ceci

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 supprimer la fonction wrapper autour de DoGetErrors et déplacer simplement UnWrap vers le site d'appel.

3
Frank Bryce

Oui, il est possible de renvoyer toutes les erreurs à la fois. Renvoyez simplement un List<T> ou ReadOnlyCollection<T>.

En renvoyant un IEnumerable<T> vous retournez une séquence de quelque chose. À première vue, cela peut sembler identique au retour de la collection, mais il y a un certain nombre de différences, vous devez garder à l'esprit.

Les collections

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

Les séquences

  • Peut être énuméré - et c'est à peu près tout ce que nous pouvons dire avec certitude.
  • Une séquence retourné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 renvoyer IEnumerable<T> permet une évaluation paresseuse, renvoyant List<T> ne fait pas).
  • Une séquence peut être infinie et laisser ainsi à l'appelant le soin de décider du nombre d'éléments à retourner.
3
Brian Rasmussen