Pourquoi utiliser le mot-clé yield, alors que je pourrais simplement utiliser un IEnumerable ordinaire?

171

Compte tenu de ce code:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            yield return item;
    }
}

Pourquoi ne devrais-je pas simplement le coder de cette façon?:

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

Je comprends en quelque sorte ce que fait le yieldmot-clé. Il dit au compilateur de construire un certain type de chose (un itérateur). Mais pourquoi l'utiliser? À part que ce soit un peu moins de code, qu'est-ce que ça fait pour moi?

James P. Wright
la source
28
Je me rends compte que ce n'est qu'un exemple, mais vraiment, le code devrait être écrit comme ceci: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft
Article connexe - À quoi sert le mot clé yield en C #?
RBT

Réponses:

241

Utiliser yieldrend la collection paresseuse.

Disons que vous n'avez besoin que des cinq premiers éléments. À votre façon, je dois parcourir toute la liste pour obtenir les cinq premiers éléments. Avec yield, je ne boucle que les cinq premiers éléments.

Robert Harvey
la source
14
Notez que l'utilisation FullList.Where(IsItemInPartialList)sera tout aussi paresseuse. Seulement, il nécessite beaucoup moins de code personnalisé --- gunk --- généré par le compilateur. Et moins de temps pour les développeurs pour l'écriture et la maintenance. (Bien sûr, c'était juste cet exemple)
sehe
4
C'est Linq, n'est-ce pas? J'imagine que Linq fait quelque chose de très similaire sous les couvertures.
Robert Harvey
1
Oui, Linq a utilisé l'exécution différée ( yield return) dans la mesure du possible.
Chad Schouggins
11
N'oubliez pas que si l'instruction yield return ne s'exécute jamais, vous obtenez toujours un résultat de collection vide, vous n'avez donc pas à vous soucier d'une exception de référence nulle. le rendement est génial avec des pépites de chocolat.
Razor
127

L'avantage des blocs d'itérateur est qu'ils fonctionnent paresseusement. Vous pouvez donc écrire une méthode de filtrage comme celle-ci:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

Cela vous permettra de filtrer un flux aussi longtemps que vous le souhaitez, sans jamais mettre en mémoire tampon plus d'un élément à la fois. Si vous n'avez besoin que de la première valeur de la séquence renvoyée, par exemple, pourquoi voudriez-vous tout copier dans une nouvelle liste?

Comme autre exemple, vous pouvez facilement créer un flux infini à l' aide de blocs d'itérateur. Par exemple, voici une séquence de nombres aléatoires:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

Comment stockeriez-vous une séquence infinie dans une liste?

Ma série de blogs Edulinq donne un exemple d'implémentation de LINQ to Objects qui fait un usage intensif des blocs d'itérateur. LINQ est fondamentalement paresseux là où il peut être - et mettre des choses dans une liste ne fonctionne tout simplement pas de cette façon.

Jon Skeet
la source
1
Je ne sais pas si vous aimez RandomSequenceou non. Pour moi, IEnumerable signifie - d'abord et avant tout - que je peux itérer avec foreach, mais cela conduirait évidemment à une boucle infinie ici. Je considérerais cela comme une mauvaise utilisation assez dangereuse du concept IEnumerable, mais YMMV.
Sebastian Negraszus
5
@SebastianNegraszus: Une séquence de nombres aléatoires est logiquement infinie. Vous pouvez facilement créer une IEnumerable<BigInteger>représentation de la séquence de Fibonacci, par exemple. Vous pouvez l'utiliser foreachavec, mais rien ne IEnumerable<T>garantit qu'il sera fini.
Jon Skeet
42

Avec le code «liste», vous devez traiter la liste complète avant de pouvoir la passer à l'étape suivante. La version "yield" passe immédiatement l'article traité à l'étape suivante. Si cette "prochaine étape" contient un ".Take (10)" alors la version "yield" ne traitera que les 10 premiers éléments et oubliera le reste. Le code "liste" aurait tout traité.

Cela signifie que vous voyez la plus grande différence lorsque vous devez effectuer beaucoup de traitement et / ou avoir de longues listes d'éléments à traiter.

Hans Ke st ing
la source
23

Vous pouvez utiliser yieldpour renvoyer des éléments qui ne figurent pas dans une liste. Voici un petit exemple qui pourrait parcourir une liste à l'infini jusqu'à son annulation.

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

Cela écrit

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

...etc. à la console jusqu'à son annulation.

Jason Whitted
la source
10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Lorsque le code ci-dessus est utilisé pour parcourir FilteredList () et en supposant que item.Name == "James" sera satisfait sur le 2ème élément de la liste, la méthode utilisant yielddonnera deux fois. C'est un comportement paresseux.

Où, la méthode utilisant list ajoutera tous les n objets à la liste et passera la liste complète à la méthode appelante.

C'est exactement un cas d'utilisation où la différence entre IEnumerable et IList peut être mise en évidence.

humblelistener
la source
7

Le meilleur exemple du monde réel que j'ai vu pour l'utilisation de yieldserait de calculer une séquence de Fibonacci.

Considérez le code suivant:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

Cela retournera:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

C'est bien car cela vous permet de calculer rapidement et facilement une série infinie, vous donnant la possibilité d'utiliser les extensions Linq et d'interroger uniquement ce dont vous avez besoin.

Middas
la source
5
Je ne vois rien de «monde réel» dans un calcul de séquence de Fibonacci.
Nek
Je suis d'accord que ce n'est pas vraiment "du monde réel", mais quelle bonne idée.
Casey
1

pourquoi utiliser [yield]? À part que ce soit un peu moins de code, qu'est-ce que ça fait pour moi?

Parfois, c'est utile, parfois non. Si l'ensemble complet de données doit être examiné et renvoyé, il n'y aura aucun avantage à utiliser le rendement, car il n'a fait qu'introduire des frais généraux.

Lorsque le rendement brille vraiment, c'est quand seul un ensemble partiel est renvoyé. Je pense que le meilleur exemple est le tri. Supposons que vous ayez une liste d'objets contenant une date et un montant en dollars de cette année et que vous aimeriez voir les premiers (5) enregistrements de l'année.

Pour ce faire, la liste doit être triée par ordre croissant de date, puis avoir les 5 premières prises. Si cela était fait sans rendement, la liste entière devrait être triée, jusqu'à s'assurer que les deux dernières dates étaient dans l'ordre.

Cependant, avec le rendement, une fois les 5 premiers articles établis, le tri s'arrête et les résultats sont disponibles. Cela peut gagner beaucoup de temps.

Travis J
la source
0

L'instruction yield return vous permet de ne renvoyer qu'un seul article à la fois. Vous collectez tous les éléments d'une liste et renvoyez à nouveau cette liste, ce qui représente une surcharge de mémoire.

Prabhavith
la source