Quel est le but ou l'avantage d'utiliser des itérateurs de rendement en C#?
tous les exemples que j'ai vus de l'utilisation de yield return x;
à l'intérieur d'une méthode C# pourraient être faits de la même manière en retournant simplement la liste entière. Dans ces cas, y a-t-il un avantage ou un avantage à utiliser la syntaxe yield return
plutôt que de retourner la liste?
aussi, dans quels types de scénarios yield return
serait utilisé que vous ne pouvez pas simplement retourner la liste complète?
10 réponses
mais si vous construisiez vous-même une collection?
en général, les itérateurs peuvent être utilisés pour générer une séquence d'objets . Par exemple, la méthode Enumerable.Range
n'a aucun type de collecte interne. Il génère juste le numéro suivant sur demande . Il y a de nombreuses utilisations à cette génération de séquence paresseuse en utilisant une machine d'état. La plupart d'entre eux sont couverts par la programmation fonctionnelle les concepts de .
à mon avis, si vous regardez les itérateurs juste comme un moyen d'énumérer à travers une collection (c'est juste un des cas d'utilisation les plus simples), vous allez dans la mauvaise direction. Comme je l'ai dit, les itérateurs sont des moyens pour retourner des séquences. La séquence pourrait même être infinie . Il n'y aurait aucun moyen de retourner une liste de longueur infinie et d'utiliser les 100 premiers éléments. Il a d'être paresseux parfois. Retourner une collection est considérablement différent de retourner un générateur de collection (qui est ce qu'un itérateur est). 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 à 10000. 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 retourner une liste de tous les nombres premiers parce que la séquence est infini et le consommateur ne sait même pas combien d'articles il veut depuis le début.
les bonnes réponses ici suggèrent qu'un avantage de yield return
est que vous n'avez pas besoin de créer une liste ; listes peuvent être coûteux. (Aussi, après un certain temps, vous les trouverez volumineux et inélégant.)
mais si vous n'avez pas de liste?
yield return
vous permet de parcourir structures de données (pas nécessairement des listes) de plusieurs façons. Par exemple, si votre objet est un arbre, vous pouvez parcourir les noeuds en pré - ou post - ordre sans créer d'autres listes ou modifier 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;
}
Évaluation Paresseuse/Exécution Différée
les blocs itérateurs" rendement "n'exécuteront aucun du code jusqu'à ce que vous appeliez réellement pour ce résultat spécifique. Cela signifie qu'ils peuvent aussi être enchaînés efficacement. Pop quiz: si la fonction "ReadLines ()" lit toutes les lignes d'un fichier texte et est implémentée en utilisant un bloc itérateur, combien de fois le code suivant itérera-t-il sur le fichier?
var query = 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 jusqu'à ce que dans la boucle foreach
.
séparation des préoccupations
encore une fois en utilisant la fonction hypothétique ReadLines()
d'en haut, nous pouvons facilement séparer le code qui lit le fichier du code qui filtre les lignes inutiles du code qui analyse réellement les résultats. Cette première, surtout, est très réutilisable.
Listes Infinies
voir ma réponse à cette question pour un bon exemple:
c # fibonacci fonction returning errors
fondamentalement, j'implémente la séquence de fibonacci en utilisant un bloc itérateur qui ne s'arrêtera jamais (du moins pas avant D'atteindre MaxInt), et puis j'utilise cette implémentation d'une manière sûre.
Sémantique Améliorée
C'est l'une de ces choses qui est beaucoup plus difficile à expliquer avec prose qu'il est à juste qui avec un simple visuel 1 :
si vous ne pouvez pas voir l'image, elle montre deux versions du même code, avec des highlights de fond pour des préoccupations différentes. Le code linq a toutes les couleurs bien groupées, tandis que le code impératif traditionnel a les couleurs entremêlées. L'auteur fait valoir (et je suis d'accord) que ce résultat est typique de l'utilisation de linq vs utilisant impératif code... que linq fasse un meilleur travail d'organisation de votre code pour avoir un meilleur flux entre les sections.
1 je crois que c'est la source originale: https://twitter.com/mariofusco/status/571999216039542784 . Notez aussi que ce code est Java, mais le C# serait similaire.
parfois, les séquences que vous devez retourner sont tout simplement trop grandes pour rentrer dans la mémoire. Par exemple, il y a environ trois mois, j'ai participé à un projet de migration de données entre les bases de données MS SLQ. Les données ont été exportées en format XML. Yield return s'est avéré très utile avec XmlReader . Cela rendait la programmation plus facile. Par exemple, supposons qu'un fichier ait 1000 client éléments-si vous venez de lire ce fichier dans mémoire, cela exigera de les stocker tous dans la mémoire en même temps, même s'ils sont manipulés séquentiellement. Ainsi, vous pouvez utiliser des itérateurs pour parcourir la collection un par un. Dans ce cas, vous avez passer de la mémoire pour un élément.
comme il s'est avéré, en utilisant XmlReader pour notre projet était le seul moyen de faire fonctionner l'application - il a fonctionné pendant une longue période, mais au moins il n'a pas accroché l'ensemble du système et n'a pas soulevé 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 si rapidement et sans problèmes). Regardez cette page pour 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).
dans les scénarios jouets/démonstration, il n'y a pas beaucoup de différence. Mais il y a des situations où il est utile de produire des itérateurs - parfois, la liste entière n'est pas disponible (par exemple, les flux), ou la liste est coûteuse sur le plan informatique et peu susceptible d'être nécessaire dans son intégralité.
si la liste entière est gigantesque, il pourrait manger beaucoup de mémoire juste pour s'asseoir autour, alors qu'avec le rendement vous jouez seulement avec ce que vous avez besoin, quand vous en avez besoin, indépendamment de combien d'articles il ya.
jetez un oeil à cette discussion sur le blog D'Eric White (excellent blog d'ailleurs) sur lazy versus eager evaluation .
en utilisant le yield return
vous pouvez itérer sur les articles sans jamais avoir à construire une liste. Si vous n'avez pas besoin de la liste, mais que vous voulez itérer sur un ensemble 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 oui ou non vous voulez opérer sur foo à l'intérieur de votre méthode en utilisant des rendements et vous foreach boucle peut être beaucoup plus concis.
voici ma réponse acceptée à exactement la même question:
une Autre façon de regarder itérateur méthodes, c'est qu'ils font le travail difficile de transformer un algorithme "à l'intérieur". Envisager un analyseur. Il tire du texte d'un flux, cherche des motifs et génère une description logique de haut niveau du contenu.
maintenant, je peux rendre cela facile pour moi-même comme un analyseur auteur en prenant l'approche SAX, dans laquelle je dispose d'une interface de rappel que je signale 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 handler et donc ils doivent écrire une classe de machine d'état qui répond aux méthodes de callback. C'est difficile d'être juste, donc la chose la plus facile à faire do est d'utiliser une implémentation de stock qui construit un arbre DOM, et alors ils auront la commodité d'être en mesure de marcher l'arbre. Mais alors toute la structure est tamponnée dans la mémoire - pas bon.
mais pourquoi pas plutôt que j'écrive mon analyseur comme méthode itératrice?
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...
}
qui ne sera pas plus difficile à écrire que l'approche callback-interface-il suffit de rendre retour un objet dérivé de ma classe de base LanguageElement
au lieu d'appeler un callback méthode.
l'utilisateur peut maintenant utiliser foreach à boucle à travers la sortie de mon analyseur, de sorte 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 être en contrôle , et sont donc plus faciles à écrire et à comprendre.
la raison fondamentale de l'utilisation de yield est qu'il génère/retourne une liste par lui-même. Nous pouvons utiliser la liste retournée pour itérer davantage.