2010-04-29 19 views
6

Donc, cette question vient d'être posée sur le SO:Quelqu'un peut-il s'il vous plaît expliquer ce code d'évaluation paresseux?

How to handle an "infinite" IEnumerable?

Mon exemple de code:

public static void Main(string[] args) 
{ 
    foreach (var item in Numbers().Take(10)) 
     Console.WriteLine(item); 
    Console.ReadKey(); 
} 

public static IEnumerable<int> Numbers() 
{ 
    int x = 0; 
    while (true) 
     yield return x++; 
} 

Quelqu'un peut-il s'il vous plaît expliquer pourquoi il en est paresseux évalué? J'ai recherché ce code dans Reflector, et je suis plus confus que quand j'ai commencé.

sorties réflecteur:

public static IEnumerable<int> Numbers() 
{ 
    return new <Numbers>d__0(-2); 
} 

Pour la méthode des nombres, et semble avoir généré un nouveau type pour cette expression:

[DebuggerHidden] 
public <Numbers>d__0(int <>1__state) 
{ 
    this.<>1__state = <>1__state; 
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId; 
} 

Cela n'a aucun sens pour moi. J'aurais supposé que c'était une boucle infinie jusqu'à ce que je mette ce code ensemble et l'ai exécuté moi-même.

EDIT: Je comprends maintenant que .Take() peut dire foreach que l'énumération est « terminée », quand il a vraiment pas, mais ne devrait pas les numéros() être appelé dans son intégralité avant enchaînant transmettre à la prise()? Le résultat de Take est ce qui est réellement énuméré, correct? Mais comment est-ce que Take s'exécute quand Numbers n'a pas complètement évalué?

EDIT2: Alors est-ce juste une astuce de compilateur spécifique imposée par le mot-clé 'yield'?

Répondre

1

La raison pour laquelle ce n'est pas une boucle infinie est que vous n'énumérez que 10 fois selon l'utilisation de l'appel Take (10) de Linq. Maintenant, si vous avez écrit le code quelque chose comme:

foreach (var item in Numbers()) 
{ 
} 

Maintenant, c'est une boucle infinie parce que votre recenseur retournera toujours une nouvelle valeur. Le compilateur C# prend ce code et le transforme en machine d'état. Si votre énumérateur n'a pas de clause de garde pour rompre l'exécution, alors l'appelant doit savoir ce qu'il fait dans votre échantillon.

La raison pour laquelle le code est paresseux est également une raison pour laquelle le code fonctionne. Essentiellement Take renvoie le premier élément, puis votre application consomme, puis il en prend un autre jusqu'à ce qu'il ait pris 10 éléments.

Modifier

Cela n'a en fait rien à voir avec l'ajout de prendre. Ceux-ci sont appelés Iterators. Le compilateur C# effectue une transformation compliquée sur votre code en créant un énumérateur à partir de votre méthode. Je recommande de lire dessus mais fondamentalement (et ceci peut ne pas être exact à 100%), votre code entrera la méthode de nombres que vous pourriez envisager comme initilizing la machine d'état. Une fois que votre code atteint un rendement de rendement, vous dites essentiellement que Numbers() cesse de s'exécuter et lui redonne ce résultat, puis lorsqu'il demande l'exécution de l'élément suivant, il reprend à la ligne suivante après le retour de rendement.

Erik Lippert has a great series sur les aspects divers de Itérateurs

+0

Oui, mais Numbers() lui-même ne devrait-il pas être entièrement évalué afin de continuer avec Take Call? Je comprends que Numbers lui-même est une boucle infinie. Pourquoi l'ajout de .Take() arrête-t-il soudainement les nombres d'être évalués dans leur intégralité? – Tejs

+0

Intéressant. Merci pour le lien! Cette question (et le code) m'a fait faire une double prise, et je réalise maintenant que j'ai plus à apprendre sur Enumerables! – Tejs

2

Cela doit avec:

  • Que IEnumerable fait quand certaines méthodes sont appelées
  • La nature de l'énumération et la déclaration de rendement

Lorsque vous énumérez sur tout type de IEnumerable, la classe vous donne le prochain article que ça va vous donner. Il ne fait rien à tous ses articles, il vous donne juste l'article suivant. Il décide de ce que cet élément va être. (Par exemple, certaines collections sont commandées, d'autres non, certaines ne garantissent pas une commande particulière, mais semblent toujours les restituer dans le même ordre que vous les avez mises en place.).

La méthode d'extension IEnumerable Take() va énumérer 10 fois, obtenant les 10 premiers éléments. Vous pourriez faire Take(100000000), et cela vous donnerait beaucoup de chiffres. Mais vous faites juste Take(10). Il demande simplement Numbers() pour l'article suivant. . . 10 fois.

Chacun de ces 10 articles, Numbers donne l'article suivant. Pour comprendre comment, vous aurez besoin de lire sur l'instruction Yield. C'est syntactic sugar pour quelque chose de plus compliqué. Yield est très puissant. (Je suis un développeur VB et je suis très ennuyé que je ne l'ai toujours pas.) Ce n'est pas une fonction; c'est un mot-clé avec certaines restrictions. Et il est beaucoup plus facile de définir un agent recenseur qu'il ne le serait autrement.

D'autres méthodes d'extension IEnumerable parcourent toujours chaque élément. Appeler. AsList ferait exploser. En l'utilisant la plupart des requêtes LINQ le ferait exploser.

+0

Écrire des enquêteurs personnalisés en utilisant le rendement est très amusant. – JoshBerke

0

Fondamentalement, votre fonction Numbers() crée un énumérateur.
Le foreach vérifiera, à chaque itération, si l'énumérateur a atteint la fin, et sinon, il continuera. Votre énumrateur praticien ne finira jamais, mais cela n'a pas d'importance. Ceci est paresseusement évalué.
L'énumérateur générera les résultats "en direct". Cela signifie que si vous écrivez .Take (3), la boucle ne sera exécutée que trois fois. L'énumérateur aurait encore quelques éléments "left" dedans, mais ils ne seraient pas générés puisqu'aucune méthode n'en a besoin, pour le moment.
Si vous essayez de générer tous les nombres de 0 à l'infini comme la fonction l'indique, et les renvoie tous en même temps, ce programme, qui n'en utilise que 10, serait beaucoup, beaucoup plus lent. C'est le bénéfice de l'évaluation paresseuse - ce qui n'est jamais utilisé n'est jamais calculé.