Renvoie tous les énumérables avec yield return à la fois; sans boucler

164

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 GetMoreErrorssans avoir à les énumérer?

Penser à cela est probablement une question stupide, mais je veux m'assurer de ne pas me tromper.

John Oxley
la source
Je suis heureux (et curieux!) De voir se poser d'autres questions sur le rendement - je ne comprends pas très bien moi-même. Pas une question stupide!
JoshJordan
C'est quoi GetCardProductionValidationErrorsFor?
Andrew Hare
4
ce qui ne va pas avec le retour GetMoreErrors (carte); ?
Sam Saffron
10
@Sam: "retourne plus pour plus d'erreurs de validation"
Jon Skeet
1
Du point de vue d'un langage non ambigu, un problème est que la méthode ne peut pas savoir s'il y a quelque chose qui implémente à la fois T et IEnumerable <T>. Vous avez donc besoin d'une construction différente dans le rendement. Cela dit, ce serait bien d'avoir un moyen de le faire. Yield return yield foo, peut-être, où foo implémente IEnumerable <T>?
William Jockusch

Réponses:

141

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

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

Cependant, si vous avez plusieurs méthodes retournant chacune un IEnumerable<ErrorInfo>, vous pouvez utiliser Enumerable.Concatpour simplifier votre code:

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

Il y a 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 les itérateurs renvoyés qu'un par un. Votre code existant attendra d'être bouclé sur tout ce qu'il contient GetMoreErrors()avant même de poser des questions sur les prochaines erreurs.

Généralement, ce n'est pas important, mais cela vaut la peine de comprendre ce qui va se passer quand.

Jon Skeet
la source
3
Wes Dyer a un article intéressant mentionnant ce modèle. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Correction mineure pour les passants - c'est System.Linq.Enumeration.Concat <> (premier, deuxième). Pas IEnumeration.Concat ().
redcalx
@ the-locster: Je ne suis pas sûr de ce que tu veux dire. C'est définitivement énumérable plutôt que énumération. Pouvez-vous clarifier votre commentaire?
Jon Skeet
@Jon Skeet - Que voulez-vous dire exactement par l'appel des méthodes immédiatement? J'ai exécuté un test et il semble que cela reporte complètement les appels de méthode jusqu'à ce que quelque chose soit réellement itéré. Code ici: pastebin.com/0kj5QtfD
Steven Oxley
5
@Steven: Non. Il appelle les méthodes - mais dans votre cas GetOtherErrors()(etc.) reportent leurs résultats (car ils sont implémentés à l'aide de blocs d'itérateur). Essayez de les changer pour renvoyer un nouveau tableau ou quelque chose comme ça, et vous verrez ce que je veux dire.
Jon Skeet
26

Vous pouvez configurer toutes les sources d'erreur comme celle-ci (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 en GetErrorSourcessera également retardée.

Adam Boddington
la source
16

Je suis venu avec un yield_extrait rapide :

animation d'utilisation de yield_ snipped

Voici 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>
John Gietzen
la source
2
En quoi est-ce une réponse à la question?
Ian Kemp
1
@Ian, voici comment vous devez faire un rendement de rendement imbriqué en C #. Il n'y a pas yield!, comme en F #.
John Gietzen
ce n'est pas une réponse à la question
divyang4481
8

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 appelé, donc quand vous l'avez dans la boucle foreach comme ça, chaque fois qu'il est appelé, il renvoie 1 élément. Vous avez la possibilité de mettre 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, elle continuera à 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;
    }
  }
}
Tim Jarvis
la source
4

Je suis surpris que personne n'ait pensé à recommander une méthode d'extension simple 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'elles est que l'empreinte mémoire est faible, même pour des é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 tu pourrais l'utiliser dans ton cas comme ça

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 DoGetErrorset vous déplacer simplement UnWrapvers le site d'appel.

Frank Bryce
la source
2
Personne n'a probablement pensé à une méthode d'extension parce qu'elle DoGetErrors(card).SelectMany(x => x)fait de même et préserve le comportement différé. C'est exactement ce qu'Adam suggère dans sa réponse .
huysentruitw
3

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

En retournant un, IEnumerable<T>vous retournez une séquence de quelque chose. En apparence, 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 est 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 du retour.
  • La collection est de taille finie.

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 que le retour IEnumerable<T>permet une évaluation paresseuse, le retour List<T>ne le permet pas).
  • Une séquence peut être infinie et donc laisser à l'appelant le soin de décider du nombre d'éléments à renvoyer.
Brian Rasmussen
la source
Le renvoi d'une collection peut entraîner une surcharge déraisonnable si tout ce dont le client a vraiment besoin est de l'énumérer, puisque vous allouez à l'avance les structures de données pour tous les éléments. De plus, si vous déléguez à une autre méthode qui renvoie une séquence, sa capture en tant que collection implique une copie supplémentaire, et vous ne savez pas combien d'éléments (et donc combien de frais généraux) cela peut potentiellement impliquer. Ainsi, ce n'est qu'une bonne idée de retourner la collection lorsqu'elle est déjà là et peut être retournée directement sans copie (ou encapsulée en lecture seule). Dans tous les autres cas, la séquence est un meilleur choix
Pavel Minaev
Je suis d'accord, et si vous avez l'impression que j'ai dit que retourner une collection est toujours une bonne idée, vous avez manqué mon point. J'essayais de souligner le fait qu'il existe des différences entre le retour d'une collection et le retour d'une séquence. J'essaierai de clarifier les choses.
Brian Rasmussen