Quand ne pas utiliser le rendement (retour)

Cette question a déjà une réponse ici:
Y a-t-il une raison de ne pas utiliser le “rendement” lors du retour d’un IEnumerable?

Il y a plusieurs questions utiles ici sur SO concernant les avantages du yield return . Par exemple,

Je cherche à savoir quand il ne faut PAS utiliser le yield return . Par exemple, si je pense devoir renvoyer tous les éléments d’une collection, cela ne semble pas être utile, n’est-ce pas?

Dans quels cas l’utilisation du yield sera-t-elle limitée, inutile, me causera des problèmes ou devrait-elle être évitée?

Dans quels cas l’utilisation du rendement sera-t-elle limitée, inutile, me causera des problèmes ou devrait-elle être évitée?

C’est une bonne idée de bien réfléchir à votre utilisation du «rendement» lorsque vous traitez avec des structures définies récursivement. Par exemple, je vois souvent ceci:

 public static IEnumerable PreorderTraversal(Tree root) { if (root == null) yield break; yield return root.Value; foreach(T item in PreorderTraversal(root.Left)) yield return item; foreach(T item in PreorderTraversal(root.Right)) yield return item; } 

Code parfaitement sensible, mais il a des problèmes de performance. Supposons que l’arbre soit profond. Ensuite, il y aura à la plupart des points des iterators nesteds O (h) construits. L’appel de “MoveNext” sur l’iterator externe fera alors O (h) des appels nesteds à MoveNext. Comme il fait ceci O (n) fois pour un arbre avec n éléments, cela donne l’algorithme O (hn). Et comme la hauteur d’un arbre binary est lg n <= h <= n, cela signifie que l'algorithme est au mieux O (ng n) et au pire O (n 2) dans le temps, et le meilleur cas O (lg n) et pire cas O (n) dans l'espace de pile. Il s'agit de O (h) dans l'espace mémoire car chaque énumérateur est alloué sur le tas. (Sur les implémentations de C #, je suis au courant; une implémentation conforme peut avoir d'autres caractéristiques d'espace de pile ou de tas.)

Mais itérer un arbre peut être O (n) dans le temps et O (1) dans un espace de stack. Vous pouvez écrire ceci comme:

 public static IEnumerable PreorderTraversal(Tree root) { var stack = new Stack>(); stack.Push(root); while (stack.Count != 0) { var current = stack.Pop(); if (current == null) continue; yield return current.Value; stack.Push(current.Left); stack.Push(current.Right); } } 

qui utilise toujours le rendement, mais est beaucoup plus intelligent à ce sujet. Maintenant, nous sums O (n) dans le temps et O (h) dans l’espace tas, et O (1) dans l’espace stack.

Lectures complémentaires: voir l’article de Wes Dyer sur le sujet:

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx

Dans quels cas l’utilisation du rendement sera-t-elle limitée, inutile, me causera des problèmes ou devrait-elle être évitée?

Je peux penser à quelques cas, IE:

  • Évitez d’utiliser le rendement lorsque vous retournez un iterator existant. Exemple:

     // Don't do this, it creates overhead for no reason // (a new state machine needs to be generated) public IEnumerable GetKeys() { foreach(ssortingng key in _someDictionary.Keys) yield return key; } // DO this public IEnumerable GetKeys() { return _someDictionary.Keys; } 
  • Évitez d’utiliser le rendement lorsque vous ne souhaitez pas différer le code d’exécution de la méthode. Exemple:

     // Don't do this, the exception won't get thrown until the iterator is // iterated, which can be very far away from this method invocation public IEnumerable Foo(Bar baz) { if (baz == null) throw new ArgumentNullException(); yield ... } // DO this public IEnumerable Foo(Bar baz) { if (baz == null) throw new ArgumentNullException(); return new BazIterator(baz); } 

L’important est de savoir à quoi sert le yield , vous pouvez alors décider quels cas n’en bénéficient pas.

En d’autres termes, lorsque vous n’avez pas besoin d’une séquence à évaluer paresseusement, vous pouvez ignorer l’utilisation du yield . Quand cela serait-il? Ce serait quand vous ne vous dérange pas d’avoir immédiatement toute votre collection en mémoire. Sinon, si vous avez une séquence énorme qui pourrait avoir un impact négatif sur la mémoire, vous devriez utiliser le yield pour travailler dessus pas à pas (par exemple, paresseusement). Un profileur peut être utile pour comparer les deux approches.

Notez que la plupart des instructions LINQ renvoient un IEnumerable . Cela nous permet de regrouper continuellement différentes opérations LINQ sans impact négatif sur les performances à chaque étape (exécution différée). L’image alternative serait de mettre un appel ToList() entre chaque instruction LINQ. Cela ferait que chaque instruction LINQ précédente serait immédiatement exécutée avant d’exécuter l’instruction LINQ suivante (chaînée), renonçant ainsi à tout avantage de l’évaluation différée et à l’utilisation de IEnumerable till nécessaire.

Il y a beaucoup d’excellentes réponses ici. J’appendais celui-ci: n’utilisez pas de rendement pour des collections petites ou vides où vous connaissez déjà les valeurs:

 IEnumerable GetSuperUserRights() { if(SuperUsersAllowed) { yield return UserRight.Add; yield return UserRight.Edit; yield return UserRight.Remove; } } 

Dans ces cas, la création de l’object Enumerator est plus coûteuse et plus détaillée que la simple génération d’une structure de données.

 IEnumerable GetSuperUserRights() { return SuperUsersAllowed ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove} : Enumerable.Empty(); } 

Mettre à jour

Voici les résultats de mon benchmark :

Résultats de référence

Ces résultats montrent combien de temps il a fallu (en millisecondes) pour effectuer l’opération 1 000 000 de fois. Les plus petits nombres sont meilleurs.

En revenant sur cette question, la différence de performance n’est pas assez importante pour vous inquiéter, alors vous devriez choisir ce qui est le plus facile à lire et à maintenir.

Eric Lippert soulève un bon point (dommage que C # n’ait pas d’ aplatissement de stream comme Cw ). J’appendais que parfois, le processus d’énumération est coûteux pour d’autres raisons, et que vous devriez donc utiliser une liste si vous avez l’intention de parcourir plus d’une fois l’IEnumerable.

Par exemple, LINQ-to-objects est construit sur “return return”. Si vous avez écrit une requête LINQ lente (par exemple, qui filtre une grande liste dans une petite liste, ou qui effectue un sorting et un regroupement), il peut être judicieux d’appeler ToList() sur le résultat de la requête pour éviter d’énumérer plusieurs times (qui exécute la requête plusieurs fois).

Si vous choisissez entre “rendement” et List lors de l’écriture d’une méthode, considérez ceci: est-ce cher et l’appelant devra-t-il énumérer les résultats plus d’une fois? Si vous savez que la réponse est oui, n’utilisez pas “return return” à moins que la liste produite ne soit extrêmement volumineuse (et vous ne pouvez pas vous permettre d’utiliser la mémoire utilisée – rappelez-vous qu’un autre avantage du yield est que la Il faut être entièrement en mémoire à la fois).

Une autre raison de ne pas utiliser le “rendement” est que les opérations d’entrelacement sont dangereuses. Par exemple, si votre méthode ressemble à ceci,

 IEnumerable GetMyStuff() { foreach (var x in MyCollection) if (...) yield return (...); } 

C’est dangereux s’il y a une chance que MyCollection change à cause de quelque chose que l’appelant fait:

 foreach(T x in GetMyStuff()) { if (...) MyCollection.Add(...); // Oops, now GetMyStuff() will throw an exception because // MyCollection was modified. } 

yield return peut causer des problèmes chaque fois que l’appelant change quelque chose que la fonction de rendement suppose ne change pas.

Le rendement serait limitatif / inutile lorsque vous avez besoin d’un access aléatoire. Si vous avez besoin d’accéder à l’élément 0 puis à l’élément 99, vous avez pratiquement éliminé l’utilité de l’évaluation différée.

Un problème qui pourrait vous surprendre est si vous sérialisez les résultats d’une énumération et les envoyez par la poste. Comme l’exécution est différée jusqu’à ce que les résultats soient nécessaires, vous sérialiserez une énumération vide et la renverrez à la place des résultats souhaités.

J’éviterais d’utiliser le yield return si la méthode a un effet secondaire que vous attendez en appelant la méthode. Ceci est dû à l’exécution différée mentionnée par Pop Catalin .

Un effet secondaire pourrait être la modification du système, ce qui pourrait se produire dans une méthode comme IEnumerable SetAllFoosToCompleteAndGetAllFoos() , qui rompt le principe de la responsabilité unique . C’est assez évident (maintenant …), mais un effet secondaire moins évident pourrait être la définition d’un résultat en cache ou une optimisation similaire.

Mes règles de base (encore, maintenant …) sont:

  • N’utiliser que les yield si l’object renvoyé nécessite un peu de traitement
  • Aucun effet secondaire dans la méthode si je dois utiliser le yield
  • Si vous devez avoir des effets secondaires (en limitant cela à la mise en cache, etc.), n’utilisez pas le yield et assurez-vous que les avantages de l’extension de l’itération dépassent les coûts

Je dois maintenir une stack de code d’un gars qui était absolument obsédé par le rendement et IEnumerable. Le problème est que de nombreuses API tierces, ainsi que beaucoup de notre propre code, dépendent des listes ou des tableaux. Je finis donc par devoir faire:

 IEnumerable myFoos = getSomeFoos(); List fooList = new List(myFoos); thirdPartyApi.DoStuffWithArray(fooList.ToArray()); 

Pas forcément mauvais, mais gênant à gérer, et à quelques resockets, cela a conduit à créer des listes en double pour éviter de tout refactoriser.

Lorsque vous ne voulez pas qu’un bloc de code retourne un iterator pour un access séquentiel à une collection sous-jacente, vous n’avez pas besoin de yield return . Vous return simplement la collection alors.

Si vous définissez une méthode d’extension Linq-y dans laquelle vous insérez des membres Linq réels, ces membres renverront le plus souvent un iterator. Céder à travers cet iterator est inutile.

Au-delà de cela, vous ne pouvez pas vraiment avoir beaucoup de difficulté à utiliser yield pour définir un énumérateur “en streaming” évalué sur une base JIT.