Le mot-clé de rendement est l’un de ces mots-clés en C # qui continue de me mystifier, et je n’ai jamais été certain de l’utiliser correctement.
Parmi les deux codes suivants, quel est le préféré et pourquoi?
Version 1: Utilisation du rendement
public static IEnumerable 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: Retourner la liste
public static IEnumerable GetAllProducts() { using (AdventureWorksEntities db = new AdventureWorksEntities()) { var products = from product in db.Product select product; return products.ToList(); } }
J’ai tendance à utiliser le rendement lorsque je calcule l’élément suivant dans la liste (ou même le groupe suivant).
En utilisant votre version 2, vous devez avoir la liste complète avant de retourner. En utilisant le rendement, il vous suffit de disposer de l’élément suivant avant de retourner.
Cela permet, entre autres, de répartir le coût 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 se rend jamais à la dernière page, vous ne calculez jamais les éléments finaux de la liste.
Un autre cas où le rendement-rendement est préférable est si IEnumerable représente un ensemble infini. Considérons la liste des nombres premiers ou une liste infinie de nombres aléatoires. Vous ne pouvez jamais renvoyer la totalité de IEnumerable en une fois. Vous utilisez donc return-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.
Remplir une liste temporaire, c’est comme télécharger toute la vidéo, alors que l’utilisation du yield
est comme la diffusion de cette vidéo.
Comme exemple conceptuel pour comprendre quand vous devez utiliser le yield
, disons que la méthode ConsumeLoop()
traite les éléments retournés / ProduceList()
par ProduceList()
:
void ConsumeLoop() { foreach (Consumable item in ProduceList()) // might have to wait here item.Consume(); } IEnumerable ProduceList() { while (KeepProducing()) yield return ProduceExpensiveConsumable(); // expensive }
Sans yield
, l’appel à ProduceList()
peut prendre du temps car vous devez compléter la liste avant de renvoyer:
//pseudo-assembly Produce consumable[0] // expensive operation, eg 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 le yield
, il se réorganise, en quelque sorte fonctionne “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
Enfin, comme beaucoup l’ont déjà suggéré, vous devez utiliser la version 2 car vous avez déjà la liste complète.
Cela va sembler une suggestion bizarre, mais j’ai appris à utiliser le mot-clé yield
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 de Python pour comprendre la présentation – je ne l’ai pas fait. Je l’ai trouvé très utile pour expliquer non seulement le fonctionnement des générateurs, mais aussi pourquoi vous devriez vous en soucier.
Je sais que c’est une vieille question, mais j’aimerais vous donner un exemple de la façon dont le mot-clé de rendement peut être utilisé de manière créative. J’ai vraiment profité de cette technique. J’espère que cela aidera quelqu’un qui trébuche sur cette question.
Remarque: Ne pensez pas au mot-clé de rendement comme étant simplement un autre moyen de créer une collection. Une grande partie de la puissance de rendement réside dans le fait que l’exécution est suspendue dans votre méthode ou votre propriété jusqu’à ce que le code d’appel répète la valeur suivante. Voici mon exemple:
L’utilisation du mot-clé yield (avec l’implémentation des coroutines Caliburn.Micro de Rob Eisenburg) me permet d’exprimer un appel asynchrone à un service Web comme celui-ci:
public IEnumerable 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’activer 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 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 à parcourir les IResults. Lorsque le premier IResult est renvoyé, l’exécution est suspendue dans HandleButtonClick () et BeginExecute () associe 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 Terminé 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 Completed = delegate { }; public bool Success { get; private set; } }
Il peut être utile de mettre en place quelque chose comme ça et de suivre 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 manières d’utiliser le rendement.
Les deux morceaux de code font vraiment deux choses différentes. La première version attirera les membres comme vous en avez besoin. La seconde version chargera tous les résultats en mémoire avant de commencer à faire quoi que ce soit avec elle.
Il n’y a pas de bonne ou de mauvaise réponse à celle-ci. Lequel est préférable dépend de la situation. Par exemple, s’il ya une limite de temps pour compléter 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 jeux de résultats volumineux, surtout si vous exécutez ce code en mode 32 bits. J’ai été mordu par des exceptions OutOfMemory plusieurs fois lors de cette méthode.
La chose clé à retenir est la suivante: les différences sont dans l’efficacité. Ainsi, vous devriez probablement choisir celui qui simplifie votre code et le modifier uniquement après le profilage.
Le rendement a deux grandes utilisations
Il aide à fournir une itération personnalisée sans créer de collections temporaires. (chargement de toutes les données et en boucle)
Cela aide à faire une itération avec état. ( diffusion)
Ci-dessous, une simple vidéo que j’ai créée avec une démonstration complète afin de supporter les deux points ci-dessus.
C’est ce que Chris Sells dit à propos de ces déclarations dans The C # Programming Language ;
J’oublie parfois que le rendement n’est pas le même que le rendement, en ce sens que le code après un rendement 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 peut être exécuté ici:
IEnumerable
F() { yield return 1; yield return 2; // Can be executed } Cela me mord souvent dans une déclaration if:
IEnumerable
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 retour.
Le rendement peut être très puissant pour les algorithmes où vous devez parcourir des millions d’objects. Considérez l’exemple suivant où vous devez calculer les voyages possibles pour le covoiturage. D’abord, nous générons des voyages possibles:
static IEnumerable CreatePossibleTrips() { for (int i = 0; i < 1000000; i++) { yield return new Trip { Id = i.ToString(), Driver = new Driver { Id = i.ToString() } }; } }
Puis parcourez chaque voyage:
static void Main(ssortingng[] args) { foreach (var sortingp in CreatePossibleTrips(sortingps)) { // possible sortingp is actually calculated only at this point, because of yield if (IsTripGood(sortingp)) { // match good sortingp } } }
Si vous utilisez List au lieu de yield, vous devrez allouer 1 million d'objects à la mémoire (~ 190mb) et cet exemple simple nécessitera environ 1400 ms. Cependant, si vous utilisez yield, vous n'avez pas besoin de mettre tous ces objects temporaires en mémoire et vous obtiendrez une vitesse d'algorithme nettement plus rapide: cet exemple ne prendra que ~ 400ms à exécuter sans aucune consommation de mémoire.
En supposant que votre classe de produits LINQ 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 consiste à convertir l’énumérateur / iterator en une liste avec la méthode ToList (). Cela signifie qu’il itère manuellement tous les éléments de l’énumérateur, puis renvoie une liste plate.
C’est un peu le point, mais comme la question est balisée, je vais lancer mes deux centimes. Pour ce genre de chose, je préfère grandement en faire une propriété:
public static IEnumerable 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 chaud, mais le code qui utilise cela sera beaucoup plus propre:
prices = Whatever.AllProducts.Select (product => product.price);
contre
prices = Whatever.GetAllProducts().Select (product => product.price);
Note: Je ne le ferais pas pour les méthodes qui peuvent prendre un certain temps pour faire leur travail.
Et qu’en est-il?
public static IEnumerable 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. Je n’ai pas VS2008 sous la main pour vérifier, cependant. Dans tous les cas, si Products implémente IEnumerable (comme il semble le faire – il est utilisé dans une déclaration foreach), je le retournerais directement.
J’aurais utilisé la version 2 du code dans ce cas. Étant donné que vous avez la liste complète des produits disponibles et que c’est ce que le “consommateur” de cet appel de méthode attend, il serait nécessaire de renvoyer les informations complètes à l’appelant.
Si l’appelant de cette méthode nécessite “une” information à la fois et que la consommation des informations suivantes est basée sur la demande, il serait alors bénéfique d’utiliser le rendement qui garantira que la commande d’exécution sera renvoyée à l’appelant lorsque une unité d’information est disponible.
Voici quelques exemples d’utilisation du rendement:
Pour répondre à vos questions, j’aurais utilisé la version 2.
Retourner la liste directement. Avantages:
Vous devriez utiliser l’iterator (yield) à partir du moment où vous pensez que vous n’aurez probablement pas à effectuer une itération complète jusqu’à la fin de la liste, ou lorsqu’il n’a pas de fin. Par exemple, l’appel du client va rechercher le premier produit satisfaisant un prédicat, vous pouvez envisager d’utiliser l’iterator, bien que ce soit un exemple artificiel, et qu’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 de l’iterator.
La phrase clé de retour de rendement est utilisée pour gérer la machine à états pour une collection particulière. Partout où le CLR voit la clé de retour rendement utilisée, CLR implémente un motif Enumerator sur cette partie de code. Ce type d’implémentation aide le développeur de tous les types de plomberie que nous aurions dû faire autrement en l’absence du mot-clé.
Supposons que le développeur filtre une collection, effectue une itération dans la collection, puis extrait ces objects dans une nouvelle collection. Ce type de plomberie est assez monotone.
Plus d’informations sur le mot-clé ici à cet article .
L’utilisation du rendement est similaire au retour du mot clé, sauf qu’il renverra un générateur . Et l’object générateur ne traversera qu’une fois .
le rendement a deux avantages:
Il y a une autre explication claire peut-être vous aider.