Utilisation appropriée du «rendement rendement»


903

Le mot-clé yield est l'un de ces mots-clés en C # qui continue de me mystifier, et je n'ai jamais été sûr de l'utiliser correctement.

Parmi les deux morceaux de code suivants, lequel est préféré et pourquoi?

Version 1: utilisation du rendement

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Version 2: renvoyer la liste

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

38
yieldest lié à IEnumerable<T>et son genre. C'est en quelque sorte une évaluation paresseuse
Jaider

voici une excellente réponse à la question similaire. stackoverflow.com/questions/15381708/…
Sanjeev Rai

1
Voici un bon exemple d'utilisation: stackoverflow.com/questions/3392612/…
ValGe

6
Je vois un bon cas d'utilisation yield returnsi le code qui réitère les résultats GetAllProducts()permet à l'utilisateur d'annuler prématurément le traitement.
JMD

2
J'ai trouvé ce fil vraiment utile: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Réponses:


806

J'ai tendance à utiliser le rendement-retour lorsque je calcule l'élément suivant dans la liste (ou même le groupe suivant d'éléments).

En utilisant votre version 2, vous devez avoir la liste complète avant de revenir. En utilisant yield-return, vous n'avez vraiment besoin que de l'article suivant avant de retourner.

Entre autres choses, cela permet de répartir le coût de calcul des calculs complexes sur une période plus longue. Par exemple, si la liste est connectée à une interface graphique et que l'utilisateur ne passe jamais à la dernière page, vous ne calculez jamais les éléments finaux de la liste.

Un autre cas où le rendement-retour est préférable est si le IEnumerable représente un ensemble infini. Considérez la liste des nombres premiers ou une liste infinie de nombres aléatoires. Vous ne pouvez jamais renvoyer l'IEnumerable complet à la fois, vous utilisez donc yield-return pour renvoyer la liste de manière incrémentielle.

Dans votre exemple particulier, vous avez la liste complète des produits, donc j'utiliserais la version 2.


31
Je dirais que dans votre exemple à la question 3, deux avantages se confondent. 1) Il répartit les coûts de calcul (parfois un avantage, parfois non) 2) Il peut paresseusement éviter indéfiniment le calcul dans de nombreux cas d'utilisation. Vous omettez de mentionner l'inconvénient potentiel qu'il maintient autour de l'état intermédiaire. Si vous avez des quantités importantes d'état intermédiaire (par exemple un HashSet pour l'élimination des doublons), l'utilisation du rendement peut gonfler votre empreinte mémoire.
Kennet Belenky

8
De plus, si chaque élément individuel est très grand, mais qu'il suffit d'y accéder de manière séquentielle, un rendement est meilleur.
Kennet Belenky

2
Et enfin ... il existe une technique légèrement loufoque mais parfois efficace pour utiliser yield pour écrire du code asynchrone sous une forme très sérialisée.
Kennet Belenky

12
Un autre exemple qui pourrait être intéressant est la lecture de fichiers CSV assez volumineux. Vous voulez lire chaque élément mais vous voulez également extraire votre dépendance. Le rendement renvoyant un IEnumerable <> vous permettra de retourner chaque ligne et de traiter chaque ligne individuellement. Pas besoin de lire un fichier de 10 Mo en mémoire. Une seule ligne à la fois.
Maxime Rouiller

1
Yield returnsemble être un raccourci pour écrire votre propre classe d'itérateur personnalisée (implémenter IEnumerator). Par conséquent, les avantages mentionnés s'appliquent également aux classes d'itérateurs personnalisés. Quoi qu'il en soit, les deux constructions conservent un état intermédiaire. Dans sa forme la plus simple, il s'agit de conserver une référence à l'objet actuel.
J. Ouwehand

641

Remplir une liste temporaire, c'est comme télécharger la vidéo entière, alors que l'utiliser, yieldc'est comme diffuser cette vidéo.


180
Je suis parfaitement conscient que cette réponse n'est pas une réponse technique, mais je pense que la ressemblance entre le rendement et le streaming vidéo sert de bon exemple pour comprendre le mot clé yield. Tout ce qui est technique a déjà été dit à ce sujet, j'ai donc essayé d'expliquer "en d'autres termes". Y a-t-il une règle communautaire qui dit que vous ne pouvez pas expliquer vos idées en termes non techniques?
anar khalilov le

13
Je ne sais pas qui vous a voté contre ou pourquoi (j'aurais aimé qu'ils aient commenté), mais je pense que cela le décrit quelque peu d'un point de vue non technique.
senfo

22
Toujours saisir le concept et cela a contribué à le mettre davantage au point, belle analogie.
Tony

11
J'aime cette réponse, mais elle ne répond pas à la question.
ANeves

73

Comme exemple conceptuel pour comprendre quand vous devez utiliser yield, disons que la méthode ConsumeLoop()traite les éléments retournés / produits par ProduceList():

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Sans yield, l'appel à ProduceList()pourrait prendre beaucoup de temps car vous devez compléter la liste avant de revenir:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

En utilisant yield, il se réorganise, sorte de travailler "en parallèle":

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

Et enfin, comme beaucoup l'ont déjà suggéré, vous devriez utiliser la version 2 car vous avez de toute façon déjà la liste complète.


30

Je sais que c'est une vieille question, mais j'aimerais donner un exemple de la façon dont le mot clé yield peut être utilisé de manière créative. J'ai vraiment profité de cette technique. J'espère que cela sera utile à quiconque trébuchera sur cette question.

Remarque: Ne pensez pas que le mot clé yield est simplement une autre façon de créer une collection. Une grande partie de la puissance du rendement vient du fait que l'exécution est suspendue dans votre méthode ou votre propriété jusqu'à ce que le code appelant répète la valeur suivante. Voici mon exemple:

L'utilisation du mot clé yield (à côté de l'implémentation Caliburn.Micro coroutines de Rob Eisenburg ) me permet d'exprimer un appel asynchrone à un service Web comme celui-ci:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Ce que cela va faire est d'allumer mon BusyIndicator, d'appeler la méthode Login sur mon service Web, de définir mon indicateur IsLoggedIn sur la valeur de retour, puis de désactiver le BusyIndicator.

Voici comment cela fonctionne: IResult a une méthode Execute et un événement Completed. Caliburn.Micro récupère l'IEnumerator de l'appel à HandleButtonClick () et le transmet à une méthode Coroutine.BeginExecute. La méthode BeginExecute commence l'itération à travers les IResults. Lorsque le premier IResult est renvoyé, l'exécution est suspendue dans HandleButtonClick () et BeginExecute () attache un gestionnaire d'événements à l'événement Completed et appelle Execute (). IResult.Execute () peut effectuer une tâche synchrone ou asynchrone et déclenche l'événement Completed lorsqu'il est terminé.

LoginResult ressemble à ceci:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Il peut être utile de configurer quelque chose comme ça et de parcourir l'exécution pour voir ce qui se passe.

J'espère que cela aide quelqu'un! J'ai vraiment aimé explorer les différentes façons dont le rendement peut être utilisé.


1
votre exemple de code est un excellent exemple sur la façon d'utiliser le rendement À L'EXTÉRIEUR d'un bloc for ou foreach. La plupart des exemples montrent le rendement de retour dans un itérateur. Très utile car j'étais sur le point de poser la question sur SO Comment utiliser le rendement en dehors d'un itérateur!
shelbypereira

Il ne m'est jamais venu à l'esprit de l'utiliser yieldde cette façon. Cela semble être une manière élégante d'émuler le modèle asynchrone / attente (qui, je suppose, serait utilisé à la place de yieldsi cela était réécrit aujourd'hui). Avez-vous constaté que ces utilisations créatives yieldont produit (sans jeu de mots) des rendements décroissants au fil des ans, car C # a évolué depuis que vous avez répondu à cette question? Ou proposez-vous toujours des cas d'utilisation intelligents modernisés comme celui-ci? Et si oui, cela vous dérangerait-il de partager un autre scénario intéressant pour nous?
Lopsided

27

Cela va sembler être une suggestion bizarre, mais j'ai appris à utiliser le yieldmot clé en C # en lisant une présentation sur les générateurs en Python: http://www.dabeaz.com/generators/Generators.pdf de David M. Beazley . Vous n'avez pas besoin de connaître beaucoup Python pour comprendre la présentation - je ne l'ai pas fait. Je l'ai trouvé très utile pour expliquer non seulement comment fonctionnent les générateurs, mais pourquoi vous devriez vous en soucier.


1
La présentation donne un aperçu simple. Ray Chen explique les détails de son fonctionnement en C # dans les liens sur stackoverflow.com/a/39507/939250. Le premier lien explique en détail qu'il existe un deuxième retour implicite à la fin des méthodes de retour de rendement.
Donal Lafferty

18

Le rendement peut être très puissant pour les algorithmes où vous devez parcourir plusieurs millions d'objets. Prenons l'exemple suivant où vous devez calculer les trajets possibles pour le covoiturage. D'abord, nous générons des voyages possibles:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Ensuite, parcourez chaque voyage:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Si vous utilisez List au lieu de yield, vous devrez allouer 1 million d'objets à la mémoire (~ 190 Mo) et cet exemple simple prendra ~ 1400 ms pour s'exécuter. Cependant, si vous utilisez yield, vous n'avez pas besoin de mettre tous ces objets temporaires en mémoire et vous obtiendrez une vitesse d'algorithme considérablement plus rapide: cet exemple ne prendra que ~ 400 ms pour fonctionner sans aucune consommation de mémoire.


2
sous les couvertures qu'est-ce que le rendement? J'aurais pensé que c'était une liste, donc comment cela améliorerait-il l'utilisation de la mémoire?
roule

1
@rolls yieldfonctionne sous les couvertures en implémentant une machine d'état en interne. Voici une réponse SO avec 3 articles de blog MSDN détaillés qui expliquent l'implémentation en détail. Écrit par Raymond Chen @ MSFT
Shiva

13

Les deux morceaux de code font vraiment deux choses différentes. La première version attirera les membres selon vos besoins. La deuxième version chargera tous les résultats en mémoire avant de commencer à en faire quoi que ce soit.

Il n'y a pas de bonne ou de mauvaise réponse à celle-ci. Laquelle est préférable dépend simplement de la situation. Par exemple, s'il y a une limite de temps pour terminer votre requête et que vous devez faire quelque chose de semi-compliqué avec les résultats, la deuxième version pourrait être préférable. Mais méfiez-vous des grands ensembles de résultats, surtout si vous exécutez ce code en mode 32 bits. J'ai été mordu par les exceptions OutOfMemory à plusieurs reprises lors de l'exécution de cette méthode.

La chose clé à garder à l'esprit est la suivante: les différences résident dans l'efficacité. Ainsi, vous devriez probablement choisir celui qui simplifie votre code et le modifier uniquement après le profilage.


11

Le rendement a deux grandes utilisations

Il permet de fournir une itération personnalisée sans créer de collections temporaires. (chargement de toutes les données et mise en boucle)

Cela aide à effectuer une itération avec état. ( diffusion)

Ci-dessous est une vidéo simple que j'ai créée avec une démonstration complète afin de soutenir les deux points ci-dessus

http://www.youtube.com/watch?v=4fju3xcm21M


10

C'est ce que Chris Sells raconte à propos de ces instructions dans le langage de programmation C # ;

J'oublie parfois que return return n'est pas la même chose que return, dans la mesure où le code après un return yield peut être exécuté. Par exemple, le code après le premier retour ici ne peut jamais être exécuté:

    int F() {
return 1;
return 2; // Can never be executed
}

En revanche, le code après le premier retour de rendement ici peut être exécuté:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

Cela me mord souvent dans une déclaration if:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

Dans ces cas, il est utile de se rappeler que le rendement n'est pas «final» comme le rendement.


pour réduire l'ambiguïté, veuillez préciser quand vous dites peut, est-ce que cela va ou peut? est-il possible que le premier retourne et n'exécute pas le second rendement?
Johno Crawford

@JohnoCrawford la deuxième instruction yield ne s'exécutera que si la deuxième / prochaine valeur de IEnumerable est énumérée. Il est tout à fait possible que cela ne se produise pas, par exemple F().Any()- cela reviendra après avoir essayé d'énumérer le premier résultat uniquement. En général, vous ne devez pas vous fier à un IEnumerable yieldpour changer l'état du programme, car il peut ne pas être déclenché
Zac Faragher

8

En supposant que la classe LINQ de vos produits utilise un rendement similaire pour l'énumération / itération, la première version est plus efficace car elle ne produit qu'une seule valeur à chaque itération.

Le deuxième exemple convertit l'énumérateur / itérateur en une liste avec la méthode ToList (). Cela signifie qu'il itère manuellement sur tous les éléments de l'énumérateur, puis renvoie une liste plate.


8

C'est un peu plus que le point, mais comme la question porte sur les meilleures pratiques, je vais continuer et mettre mes deux cents. Pour ce type de chose, je préfère grandement en faire une propriété:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Bien sûr, c'est un peu plus de plaque chauffante, mais le code qui utilise cela aura l'air beaucoup plus propre:

prices = Whatever.AllProducts.Select (product => product.price);

contre

prices = Whatever.GetAllProducts().Select (product => product.price);

Remarque: je ne ferais pas cela pour les méthodes qui peuvent prendre un certain temps pour faire leur travail.


7

Et qu'en est-il?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Je suppose que c'est beaucoup plus propre. Cependant, je n'ai pas VS2008 à portée de main pour vérifier. Dans tous les cas, si Products implémente IEnumerable (comme il semble - il est utilisé dans une instruction foreach), je le retournerais directement.


2
Veuillez modifier OP pour inclure plus d'informations au lieu de publier des réponses.
Brian Rasmussen

Eh bien, vous devez me dire ce que OP représente exactement :-) Merci
petr k.

Post d'origine, je suppose. Je ne peux pas modifier les messages, donc cela semblait être la voie à suivre.
petr k.

5

J'aurais utilisé la version 2 du code dans ce cas. Étant donné que vous disposez de la liste complète des produits disponibles et que c'est ce que le «consommateur» attend de cet appel de méthode, il serait nécessaire de renvoyer les informations complètes à l'appelant.

Si l'appelant de cette méthode requiert "une" information à la fois et que la consommation des informations suivantes est à la demande, il serait alors utile d'utiliser return return qui s'assurera que la commande d'exécution sera retournée à l'appelant lorsque une unité d'information est disponible.

Voici quelques exemples où l'on pourrait utiliser le rendement du rendement:

  1. Calcul complexe, étape par étape, où l'appelant attend les données d'une étape à la fois
  2. Pagination dans l'interface graphique - où l'utilisateur peut ne jamais atteindre la dernière page et seul un sous-ensemble d'informations doit être divulgué sur la page actuelle

Pour répondre à vos questions, j'aurais utilisé la version 2.


3

Renvoyez la liste directement. Avantages:

  • C'est plus clair
  • La liste est réutilisable. (l'itérateur n'est pas) pas vraiment vrai, merci Jon

Vous devez utiliser l'itérateur (yield) à partir du moment où vous pensez que vous n'aurez probablement pas à répéter jusqu'à la fin de la liste, ou lorsqu'il n'aura pas de fin. Par exemple, le client appelant va rechercher le premier produit qui satisfait un prédicat, vous pouvez envisager d'utiliser l'itérateur, bien que ce soit un exemple artificiel, et il existe probablement de meilleures façons de le faire. Fondamentalement, si vous savez à l'avance que toute la liste devra être calculée, faites-le tout de suite. Si vous pensez que ce ne sera pas le cas, envisagez d'utiliser la version itérateur.


N'oubliez pas qu'il revient dans IEnumerable <T>, pas dans un IEnumerator <T> - vous pouvez appeler à nouveau GetEnumerator.
Jon Skeet

Même si vous savez à l'avance que la liste entière devra être calculée, il pourrait être avantageux d'utiliser le rendement du rendement. Un exemple est lorsque la collection contient des centaines de milliers d'articles.
Val

1

La phrase clé return return est utilisée pour maintenir la machine d'état pour une collection particulière. Partout où le CLR voit la phrase clé return return utilisée, CLR implémente un modèle Enumerator sur ce morceau de code. Ce type d'implémentation aide le développeur de tout type de plomberie que nous aurions autrement dû faire en l'absence du mot-clé.

Supposons que le développeur filtre une collection, effectue une itération dans la collection, puis extrait ces objets dans une nouvelle collection. Ce type de plomberie est assez monotone.

En savoir plus sur le mot clé ici dans cet article .


-4

L'utilisation de yield est similaire au mot-clé return , sauf qu'il renvoie un générateur . Et l' objet générateur ne traversera qu'une seule fois .

le rendement a deux avantages:

  1. Vous n'avez pas besoin de lire ces valeurs deux fois;
  2. Vous pouvez obtenir de nombreux nœuds enfants mais ne devez pas tous les mettre en mémoire.

Il y a une autre explication claire qui peut vous aider.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.