web-dev-qa-db-fra.com

Quel est le but / l'avantage d'utiliser les itérateurs de rendement en C #?

Tous les exemples que j'ai vus d'utiliser yield return x; à l'intérieur d'une méthode C # pourrait être fait de la même manière en renvoyant simplement la liste entière. Dans ces cas, y a-t-il un avantage ou un avantage à utiliser le yield return syntaxe vs renvoi de la liste?

De plus, dans quels types de scénarios yield return utiliser que vous ne pouviez pas simplement renvoyer la liste complète?

76
CoderDennis

Et si vous construisiez vous-même une collection?

En général, les itérateurs peuvent être utilisés pour générer paresseusement une séquence d'objets. Par exemple Enumerable.Range La méthode n'a aucun type de collection en interne. Il génère simplement le numéro suivant sur demande. Il existe de nombreuses utilisations de cette génération de séquences paresseuses à l'aide d'une machine à états. La plupart d'entre eux sont couverts sous concepts de programmation fonctionnelle.

À mon avis, si vous regardez les itérateurs comme un moyen d'énumérer à travers une collection (ce n'est qu'un des cas d'utilisation les plus simples), vous allez dans le mauvais sens. Comme je l'ai dit, les itérateurs sont des moyens de renvoyer des séquences. La séquence pourrait même être infinie. Il n'y aurait aucun moyen de renvoyer une liste de longueur infinie et d'utiliser les 100 premiers éléments. Il a pour être paresseux parfois. Le retour d'une collection est considérablement différent du retour d'un générateur de collection (c'est ce qu'est un itérateur). C'est comparer des pommes à des oranges.

Exemple hypothétique:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

Cet exemple imprime des nombres premiers inférieurs à 10 000. Vous pouvez facilement le modifier pour imprimer des nombres inférieurs à un million sans toucher à l'algorithme de génération de nombres premiers. Dans cet exemple, vous ne pouvez pas renvoyer une liste de tous les nombres premiers car la séquence est infinie et le consommateur ne sait même pas combien d'articles il veut depuis le début.

115
Mehrdad Afshari

Les bonnes réponses suggèrent ici qu'un avantage de yield return est que vous n'avez pas besoin de créer une liste ; Les listes peuvent coûter cher. (De plus, après un certain temps, vous les trouverez encombrants et inélégants.)

Mais que faire si vous n'avez pas de liste?

yield return vous permet de parcourir structures de données (pas nécessairement Lists) de plusieurs façons. Par exemple, si votre objet est un arbre, vous pouvez parcourir les nœuds en pré ou post-ordre sans créer d'autres listes ou changer la structure de données sous-jacente.

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}
24
Ray

Évaluation paresseuse/exécution différée

Les blocs d'itérateur "return return" n'exécuteront aucun du code tant que vous n'aurez pas réellement appelé ce résultat spécifique. Cela signifie qu'ils peuvent également être enchaînés efficacement. Quiz pop: combien de fois le code suivant va-t-il parcourir le fichier?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

La réponse est exactement une, et ce pas avant la descente de la boucle foreach. Même si j'ai trois fonctions d'opérateur linq distinctes, nous ne parcourons le contenu du fichier qu'une seule fois.

Cela présente des avantages autres que les performances. Par exemple, je peux écrire une méthode assez simple et générique pour lire et pré-filtrer un fichier journal une fois, et utiliser cette même méthode à plusieurs endroits différents , où chaque utilisation ajoute des filtres différents. Ainsi, je maintiens de bonnes performances tout en réutilisant efficacement le code.

Listes infinies

Voir ma réponse à cette question pour un bon exemple:
fonction C # fibonacci renvoyant des erreurs

Fondamentalement, j'implémente la séquence fibonacci en utilisant un bloc itérateur qui ne s'arrêtera jamais (au moins, pas avant d'atteindre MaxInt), puis j'utilise cette implémentation de manière sûre.

Amélioration de la sémantique et séparation des préoccupations

En utilisant à nouveau l'exemple de fichier ci-dessus, nous pouvons maintenant séparer facilement le code qui lit le fichier du code qui filtre les lignes inutiles du code qui analyse réellement les résultats. Ce premier, en particulier, est très réutilisable.

C'est une de ces choses qui est beaucoup plus difficile à expliquer avec de la prose qu'à qui avec un simple visuel1:

Imperative vs Functional Separation of Concerns

Si vous ne pouvez pas voir l'image, elle montre deux versions du même code, avec des reflets d'arrière-plan pour différentes préoccupations. Le code linq a toutes les couleurs bien regroupées, tandis que le code impératif traditionnel a les couleurs entremêlées. L'auteur soutient (et je suis d'accord) que ce résultat est typique de l'utilisation de linq par rapport à l'utilisation de code impératif ... que linq fait un meilleur travail d'organisation de votre code pour avoir un meilleur flux entre les sections.


1 Je pense que c'est la source d'origine: https://Twitter.com/mariofusco/status/571999216039542784 . Notez également que ce code est Java, mais le C # serait similaire.

16
Joel Coehoorn

Parfois, les séquences que vous devez renvoyer sont tout simplement trop grandes pour tenir dans la mémoire. Par exemple, il y a environ 3 mois, j'ai participé à un projet de migration de données entre des bases de données MS SLQ. Les données ont été exportées au format XML. Le retour de rendement s'est avéré très utile avec XmlReader . Cela a rendu la programmation beaucoup plus facile. Par exemple, supposons qu'un fichier contienne 1000 éléments client - si vous venez de lire ce fichier en mémoire, cela nécessitera de les stocker tous en mémoire au en même temps, même s’ils sont traités séquentiellement. Ainsi, vous pouvez utiliser des itérateurs afin de parcourir la collection un par un. Dans ce cas, vous ne devez dépenser que de la mémoire pour un élément.

Il s'est avéré que l'utilisation de XmlReader pour notre projet était le seul moyen de faire fonctionner l'application - cela a fonctionné pendant longtemps, mais au moins, cela a fonctionné. pas bloqué l'ensemble du système et n'a pas déclenché OutOfMemoryException . Bien sûr, vous pouvez travailler avec XmlReader sans itérateurs de rendement. Mais les itérateurs ont rendu ma vie beaucoup plus facile (je n'écrirais pas le code pour l'importation aussi rapidement et sans problèmes). Regardez ceci page afin de voir comment les itérateurs de rendement sont utilisés pour résoudre des problèmes réels (pas seulement scientifiques avec des séquences infinies).

10
SPIRiT_1984

Dans les scénarios de jouet/démonstration, il n'y a pas beaucoup de différence. Mais il existe des situations où les itérateurs de rendement sont utiles - parfois, la liste entière n'est pas disponible (par exemple les flux), ou la liste est coûteuse en calcul et peu susceptible d'être nécessaire dans son intégralité.

9

Voici ma précédente réponse acceptée à exactement la même question:

Rendement de la valeur ajoutée du mot clé?

Une autre façon de voir les méthodes de l'itérateur est qu'elles font le travail difficile de transformer un algorithme "à l'envers". Considérez un analyseur. Il extrait le texte d'un flux, y recherche des modèles et génère une description logique de haut niveau du contenu.

Maintenant, je peux me faciliter la tâche en tant qu'auteur d'analyseur en adoptant l'approche SAX, dans laquelle j'ai une interface de rappel que je préviens chaque fois que je trouve la prochaine pièce du modèle. Donc dans le cas de SAX, chaque fois que je trouve le début d'un élément, j'appelle la méthode beginElement, et ainsi de suite.

Mais cela crée des problèmes pour mes utilisateurs. Ils doivent implémenter l'interface du gestionnaire et ils doivent donc écrire une classe de machine d'état qui répond aux méthodes de rappel. C'est difficile à faire correctement, donc la chose la plus simple à faire est d'utiliser une implémentation stock qui construit un arbre DOM, et ensuite ils auront la possibilité de marcher dans l'arbre. Mais alors, toute la structure est mise en mémoire tampon - pas bon.

Mais qu'en est-il à la place j'écris mon analyseur comme méthode d'itérateur?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

Cela ne sera pas plus difficile à écrire que l'approche de l'interface de rappel - il suffit de renvoyer un objet dérivé de ma classe de base LanguageElement au lieu d'appeler une méthode de rappel.

L'utilisateur peut maintenant utiliser foreach pour parcourir la sortie de mon analyseur, afin qu'ils obtiennent une interface de programmation impérative très pratique.

Le résultat est que les deux côtés d'une API personnalisée semblent avoir le contrôle , et sont donc plus faciles à écrire et à comprendre.

2
Daniel Earwicker

Si la liste entière est gigantesque, elle peut consommer beaucoup de mémoire juste pour s'asseoir, alors qu'avec le rendement, vous ne jouez qu'avec ce dont vous avez besoin, quand vous en avez besoin, quel que soit le nombre d'articles.

2
nilamo

Jetez un coup d'œil à cette discussion sur le blog d'Eric White (excellent blog d'ailleurs) sur évaluation paresseuse versus avide .

2
JP Alioto

En utilisant le yield return vous pouvez parcourir les éléments sans avoir à créer de liste. Si vous n'avez pas besoin de la liste, mais que vous souhaitez parcourir un certain nombre d'éléments, il peut être plus facile d'écrire

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

Que

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Vous pouvez mettre toute la logique pour déterminer si vous souhaitez ou non opérer sur foo à l'intérieur de votre méthode à l'aide des rendements et votre boucle foreach peut être beaucoup plus concise.

2
AgileJon

La raison fondamentale de l'utilisation de yield est qu'elle génère/renvoie une liste par elle-même. Nous pouvons utiliser la liste renvoyée pour réitérer davantage.

2
Tapas Ranjan Singh