web-dev-qa-db-fra.com

Quand NE PAS utiliser le rendement (retour)

Cette question a déjà une réponse ici:
Existe-t-il une raison pour ne pas utiliser 'rendement de retour' lors du renvoi d'un IEnumerable?

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

Je cherche à savoir quand [~ # ~] pas [~ # ~] utiliser yield return. Par exemple, si j’espère avoir besoin de renvoyer tous les éléments d’une collection, il ne sera pas utile semble comme yield, non?

Dans quels cas l'utilisation de yield sera-t-elle limitante, inutile, me cause-t-elle des ennuis ou devrait-elle être évitée?

157
Lawrence P. Kelley

Dans quels cas l'utilisation du rendement sera-t-elle limitante, inutile, me causera-t-elle des ennuis ou autrement devrait-elle être évitée?

Il est judicieux de bien réfléchir à votre utilisation du "rendement" lorsque vous traitez avec des structures définies de manière récursive. Par exemple, je vois souvent ceci:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> 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;
}

Un code parfaitement sensible, mais qui pose des problèmes de performances. Supposons que l'arbre soit profond. Ensuite, il y aura au maximum des O(h) itérateurs imbriqués construits. L'appel de "MoveNext" sur l'itérateur externe fera alors O(h) appels imbriqués to MoveNext. Puisqu'il fait cela O(n) fois pour un arbre avec n éléments, l'algorithme est O (hn). Et puisque la hauteur d'un arbre binaire est lg n <= h <= n, cela signifie que l’algorithme est au mieux O (n lg n) et au pire O (n ^ 2) dans le temps, et dans le meilleur des cas O (lg n) et dans le pire des cas O(n) dans l’espace pile. C’est O(h) dans l’espace tas, car chaque énumérateur est alloué sur le tas. (Sur les implémentations de C # à ma connaissance; implémentation conforme pourrait 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 l’espace de pile. Vous pouvez écrire ceci à la place de la manière suivante:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    var stack = new Stack<Tree<T>>();
    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. Nous sommes maintenant O(n) dans le temps et O(h) dans l'espace de segment de mémoire) et O(1) dans l'espace de pile.

Pour en savoir plus: voir l'article de Wes Dyer sur le sujet:

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

142
Eric Lippert

Dans quels cas l'utilisation du rendement sera-t-elle limitante, inutile, me causera-t-elle des ennuis ou autrement devrait-elle être évitée?

Je peux penser à quelques cas, par exemple:

  • Évitez d’utiliser return avec un itérateur existant. Exemple:

    // Don't do this, it creates overhead for no reason
    // (a new state machine needs to be generated)
    public IEnumerable<string> GetKeys() 
    {
        foreach(string key in _someDictionary.Keys)
            yield return key;
    }
    // DO this
    public IEnumerable<string> GetKeys() 
    {
        return _someDictionary.Keys;
    }
    
  • Évitez d’utiliser return return 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<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         yield ...
    }
    // DO this
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         return new BazIterator(baz);
    }
    
56
Pop Catalin

La chose clé à réaliser est ce que yield est utile, alors vous pouvez décider quels cas ne bénéficient pas de cela.

En d'autres termes, lorsque vous n'avez pas besoin d'une séquence pour être évaluée paresseusement, vous pouvez ignorer l'utilisation de yield. Quand serait-ce? Ce serait le cas lorsque vous ne voudriez pas que votre collection entière soit immédiatement mémorisée. Sinon, si vous avez une séquence énorme qui aurait un impact négatif sur la mémoire, vous voudriez utiliser yield pour la travailler pas à pas (c'est-à-dire paresseusement). Un profileur peut s'avérer utile lors de la comparaison des deux approches.

Notez que la plupart des instructions LINQ renvoient un IEnumerable<T>. Cela nous permet d'enchaîner continuellement différentes opérations LINQ sans impacter négativement les performances à chaque étape (exécution différée). L’image alternative consisterait à placer un appel ToList() entre chaque instruction LINQ. Ainsi, chaque instruction LINQ précédente sera immédiatement exécutée avant d'exécuter l'instruction LINQ suivante (chaînée), renonçant ainsi à tout bénéfice d'évaluation paresseuse et utilisant le IEnumerable<T> Jusqu'au besoin.

32
Ahmad Mageed

Il y a beaucoup d'excellentes réponses ici. J'ajouterais celui-ci: n'utilisez pas return pour les collections petites ou vides dont vous connaissez déjà les valeurs:

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

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

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

Mise à jour

Voici les résultats de mon point de repère :

Benchmark Results

Ces résultats indiquent le temps (en millisecondes) nécessaire pour effectuer l'opération 1 000 000 fois. Les petits nombres sont meilleurs.

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

Mise à jour 2

Je suis à peu près sûr que les résultats ci-dessus ont été obtenus avec l'optimisation du compilateur désactivée. Fonctionnant en mode Release avec un compilateur moderne, il apparaît que les performances sont pratiquement indiscernables entre les deux. Allez avec ce qui est le plus lisible pour vous.

23
StriplingWarrior

Eric Lippert soulève un bon point (dommage que C # n'ait pas le flux est aplati comme Cw ). J'ajouterais que, parfois, le processus d'énumération est coûteux pour d'autres raisons. Par conséquent, vous devriez utiliser une liste si vous avez l'intention de parcourir plusieurs fois le nombre 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 longue liste en une petite liste ou qui effectue le tri et le regroupement), il peut être judicieux d'appeler ToList() sur le résultat de la requête dans l'ordre. pour éviter d'énumérer plusieurs fois (qui exécute la requête plusieurs fois).

Si vous choisissez entre "return return" et List<T> lors de l'écriture d'une méthode, considérez que chaque élément est coûteux à calculer et l'appelant devra-t-il énumérer les résultats plusieurs fois? Si vous savez que les réponses sont oui et oui, vous ne devriez pas utiliser yield return (sauf si, par exemple, la liste produite est très longue et que vous ne pouvez pas vous permettre la mémoire qu’elle utiliserait. N'oubliez pas qu'un autre avantage de yield est que la liste des résultats ne doit pas nécessairement être entièrement insérée. mémoire à la fois).

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

IEnumerable<T> 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 a 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 modifie quelque chose que la fonction d'affichage suppose ne pas changer.

18
Qwertie

J'éviterais d'utiliser yield return Si la méthode a un effet secondaire que vous prévoyez d'appeler la méthode. Ceci est dû à l'exécution différée que Pop Catalin mentionne .

Un effet secondaire pourrait être la modification du système, ce qui pourrait se produire dans une méthode telle que IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos(), qui rompt le principe de responsabilité unique . C'est assez évident (maintenant ...), mais un effet secondaire moins évident pourrait être de définir un résultat mis en cache ou similaire sous forme d'optimisation.

Mes règles de base (encore, maintenant ...) sont les suivantes:

  • N'utilisez yield que si l'objet renvoyé nécessite un peu de traitement
  • Aucun effet secondaire dans la méthode si j'ai besoin d'utiliser yield
  • Si vous devez avoir des effets secondaires (en vous limitant à la mise en cache, etc.), n'utilisez pas yield et assurez-vous que les avantages de l'extension de l'itération l'emportent sur les coûts
6
Ben Scott

Le rendement serait limité/inutile lorsque vous avez besoin d'un accès aléatoire. Si vous devez accéder à l'élément 0, puis à l'élément 99, vous avez pratiquement éliminé l'utilité de l'évaluation paresseuse.

5
Robert Gowland

Vous pourriez par exemple vous prendre si vous sérialisez les résultats d'une énumération et que vous les envoyez par la poste. Comme l'exécution est différée jusqu'à ce que les résultats soient nécessaires, vous allez sérialiser une énumération vide et la renvoyer à la place des résultats souhaités.

5
Aidan

Je dois maintenir une pile de code d'un gars qui était absolument obsédé par le rendement des rendements et IEnumerable. Le problème est que beaucoup d'API tierces que nous utilisons, ainsi que beaucoup de notre propre code, dépendent de listes ou de tableaux. Alors je finis par avoir à faire:

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

Pas nécessairement mauvais, mais assez ennuyeux à gérer, et à quelques occasions cela a conduit à créer des listes en double dans la mémoire pour éviter de tout refactoriser.

2
Mike Ruhlin

Lorsque vous ne souhaitez pas qu'un bloc de code retourne un itérateur pour un accès séquentiel à une collection sous-jacente, vous n'avez pas besoin de yield return. Vous avez simplement return la collection alors.

2
explorer

Si vous définissez une méthode d'extension Linq-y dans laquelle vous encapsulez les membres réels de Linq, ceux-ci renverront le plus souvent un itérateur. Céder vous-même à travers cet itérateur n'est pas nécessaire.

Au-delà de cela, vous n’avez pas vraiment de problèmes pour utiliser le rendement afin de définir un énumérable "en continu" qui est évalué sur une base JIT.

0
KeithS