158 votes

Quand NE PAS utiliser le rendement (return)

Cette question a déjà une réponse ici :
Y a-t-il une raison de ne pas utiliser 'yield return' pour retourner un IEnumerable ?

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

Je cherche des idées pour savoir quand PAS à utiliser yield return . Par exemple, si je m'attends à avoir besoin de retourner tous les éléments d'une collection, il n'est pas nécessaire d'avoir recours à la fonction de retour. semblent comme yield serait utile, non ?

Quels sont les cas où l'utilisation de yield sera limitative, inutile, me causera des ennuis ou devra être évitée ?

144voto

Eric Lippert Points 300275

Quels sont les cas où l'utilisation du rendement sera limitée, inutile, me causera des ennuis ou devrait être évitée ?

Il est bon de réfléchir attentivement à l'utilisation de l'expression "yield return" lorsqu'il s'agit de structures définies par récurrence. 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;
}

Ce code a l'air parfaitement sensé, mais il pose des problèmes de performance. Supposons que l'arbre a une profondeur de h. Alors il y aura au maximum O(h) itérateurs imbriqués construits. L'appel à "MoveNext" sur l'itérateur externe fera alors O(h) appels imbriqués à MoveNext. Puisqu'il le fait O(n) fois pour un arbre de n éléments, cela rend l'algorithme 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) en temps, et au mieux O(lg n) et au pire O(n) en espace de pile. Il est O(h) dans l'espace du tas car chaque énumérateur est alloué sur le tas. (Sur les implémentations de C# que je connais ; une implémentation conforme pourrait avoir d'autres caractéristiques d'espace de pile ou de tas).

Mais l'itération d'un arbre peut être O(n) en temps et O(1) en espace de pile. Vous pouvez écrire ceci à la place comme :

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 retour de rendement, mais de manière beaucoup plus intelligente. Maintenant nous sommes O(n) en temps et O(h) en espace de tas, et O(1) en 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

56voto

Pop Catalin Points 25033

Quels sont les cas où l'utilisation du rendement sera limitative, inutile, me fera des problèmes, ou devrait être éviter ?

Je peux penser à quelques cas, IE :

  • Évitez d'utiliser yield return lorsque vous retournez 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 yield return lorsque vous ne voulez 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);
    }

32voto

Ahmad Mageed Points 44495

Ce qu'il faut comprendre, c'est que yield est utile, vous pouvez ensuite décider quels sont les cas qui n'en bénéficient pas.

En d'autres termes, lorsque vous n'avez pas besoin qu'une séquence soit évaluée paresseusement, vous pouvez éviter d'utiliser l'option yield . Quand est-ce que ce serait ? Ce serait quand cela ne vous dérange pas d'avoir immédiatement toute votre collection en mémoire. Sinon, si vous avez une énorme séquence qui aurait un impact négatif sur la mémoire, vous voudriez utiliser la fonction yield pour y travailler pas à pas (c'est-à-dire paresseusement). Un profileur peut s'avérer utile pour comparer les deux approches.

Remarquez que la plupart des instructions LINQ renvoient un IEnumerable<T> . Cela nous permet d'enchaîner continuellement différentes opérations LINQ sans nuire aux performances à chaque étape (exécution différée). L'image alternative serait de mettre un ToList() entre chaque instruction LINQ. Cela entraînerait l'exécution immédiate de chaque instruction LINQ précédente avant l'exécution de l'instruction LINQ suivante (enchaînée), renonçant ainsi à tout avantage de l'évaluation paresseuse et à l'utilisation de la fonction IEnumerable<T> jusqu'à ce que cela soit nécessaire.

23voto

StriplingWarrior Points 56276

Il y a beaucoup d'excellentes réponses ici. J'ajouterais celle-ci : N'utilisez pas yield 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 verbeuse, 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 référence :

Benchmark Results

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

En y repensant, la différence de performance n'est pas assez importante pour que l'on s'en préoccupe, et vous devriez donc opter pour ce qui est le plus facile à lire et à maintenir.

Mise à jour 2

Je suis presque sûr que les résultats ci-dessus ont été obtenus avec l'optimisation du compilateur désactivée. En mode Release avec un compilateur moderne, il semble que les performances soient pratiquement indiscernables entre les deux. Choisissez ce qui est le plus lisible pour vous.

18voto

Qwertie Points 5311

Eric Lippert soulève un bon point (dommage que le C# ne dispose pas de aplatissement du flux comme Cw ). J'ajouterais 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 d'itérer sur l'IEnumerable plus d'une fois.

Par exemple, LINQ-to-objects est construit sur le principe du "yield return". Si vous avez écrit une requête LINQ lente (par exemple, qui filtre une grande liste en une petite liste, ou qui effectue un tri et un regroupement), il peut être judicieux d'appeler ToList() sur le résultat de la requête afin d'éviter une énumération multiple (qui exécute en fait la requête plusieurs fois).

Si vous devez choisir entre le "rendement" et le "retour sur investissement", vous devez choisir entre les deux. List<T> Lorsque vous écrivez une méthode, posez-vous la question suivante : le calcul de chaque élément est-il coûteux et l'appelant aura-t-il besoin d'énumérer les résultats plus d'une fois ? Si vous savez que les réponses sont oui et oui, vous ne devriez pas utiliser la méthode yield return (sauf si, par exemple, la liste produite est très importante 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 être entièrement en mémoire en même temps).

Une autre raison de ne pas utiliser "yield return" est que l'entrelacement des opérations est dangereux. Par exemple, si votre méthode ressemble à quelque chose comme ceci,

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

Ceci est dangereux s'il y a une chance que MyCollection change à cause d'une action de l'appelant :

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 cédante suppose ne pas changer.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X