web-dev-qa-db-fra.com

Utilisation appropriée du "rendement"

Le mot-clé rendement 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 codes suivants, quel est le code 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: Retourne 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>();
    }
}
853
senfo

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

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

Cela permet notamment de répartir le coût de calcul de 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ù rendement/rendement est préférable est si 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'intégralité de IEnumerable à la fois, vous devez donc utiliser return-return pour renvoyer la liste de manière incrémentielle.

Dans votre exemple particulier, vous avez la liste complète des produits, je voudrais donc utiliser la version 2.

774
abelenky

Remplir une liste temporaire équivaut à télécharger la vidéo entière, alors que l'utilisation de yield revient à la diffuser en continu.

602
anar khalilov

En tant qu'exemple conceptuel permettant de comprendre quand vous devez utiliser yield, supposons que la méthode ConsumeLoop() traite les éléments renvoyés/renvoyés 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 de ProduceList() peut prendre un certain temps car vous devez compléter la liste avant de renvoyer:

//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 devient réarrangé, sorte de travail "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 d’entre vous l’ont déjà suggéré, vous devez utiliser la version 2, car vous avez déjà la liste complète.

67
Kache

Cela va paraître comme 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: David M. Beazley http: //www.dabeaz. com/generators/Generators.pdf . Vous n'avez pas besoin d'en savoir beaucoup Python pour comprendre la présentation - je ne l'ai pas fait. J'ai trouvé très utile d'expliquer non seulement comment fonctionnent les générateurs, mais aussi pourquoi vous devriez vous en soucier.

26
Robert Rossney

Je sais que c’est une vieille question, mais j’aimerais donner un exemple de la façon dont le mot-clé de rendement peut être utilisé de manière créative. J'ai vraiment bénéficié de cette technique. Espérons que cela sera utile à quiconque trébuche sur cette question.

Remarque: Ne pensez pas que le mot clé de rendement est 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 en pause dans votre méthode ou propriété jusqu'à ce que le code appelant parcourt la valeur suivante. Voici mon exemple:

L'utilisation du mot-clé de rendement (à côté de l'implémentation de Implantations Caliburn.Micro 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();
}

Cela va activer mon BusyIndicator, appeler la méthode de connexion sur mon service Web, définir mon indicateur IsLoggedIn sur la valeur de retour, puis éteindre le BusyIndicator.

Voici comment cela fonctionne: IResult a une méthode Execute et un événement Completed. Caliburn.Micro récupère l'IEnumerator à partir de l'appel de HandleButtonClick () et le transmet à une méthode Coroutine.BeginExecute. La méthode BeginExecute commence à parcourir les résultats IR. Lorsque le premier résultat IResult est renvoyé, l'exécution est suspendue dans HandleButtonClick () et BeginExecute () attache un gestionnaire d'événement à l'événement Completed et appelle Execute (). IResult.Execute () peut effectuer une tâche synchrone ou asynchrone et déclencher 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 d’organiser quelque chose comme cela et d’effectuer une 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.

26
Adam W. McKinley

Yield return peut être très puissant pour les algorithmes dans lesquels vous devez parcourir plusieurs millions d'objets. Prenons l'exemple suivant où vous devez calculer les trajets possibles pour un covoiturage. Tout 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(trips))
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Si vous utilisez Liste au lieu de rendement, vous devrez allouer 1 million d'objets à la mémoire (~ 190 Mo) et cet exemple simple nécessitera environ 1400 ms. Cependant, si vous utilisez le rendement, 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 s'exécuter sans aucune consommation de mémoire.

13
Andzej Maciusovic

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 vous commencez à 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 vous reste 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 peut être préférable. Mais méfiez-vous des résultats volumineux, surtout si vous exécutez ce code en mode 32 bits. J'ai été mordu par des exceptions OutOfMemory à plusieurs reprises lors de l'utilisation de cette méthode.

La chose essentielle à garder à l’esprit est cependant la suivante: les différences se situent au niveau de l’efficacité. Ainsi, vous devriez probablement choisir celui qui simplifie votre code et ne le modifiez qu’après le profilage.

12
Jason Baker

Le rendement a deux bonnes utilisations

Il est utile de fournir une itération personnalisée sans créer de collections temporaires. (chargement de toutes les données et bouclage)

Il est utile de faire une itération avec état. ( diffusion)

Ci-dessous, 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

11

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

J'oublie parfois que le retour de rendement n'est pas identique au retour, en ce sens que le code après un retour de 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 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 retour de rendement n'est pas "final", le retour est utile.

9
Teoman shipahi

En supposant que vos produits LINQ utilisent un rendement similaire pour énumérer/itérer, la première version est plus efficace car elle ne génère qu'une valeur à chaque fois.

Le deuxième exemple consiste à convertir 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 non hiérarchique.

8
Soviut

C’est un peu différent du but, mais puisque la question est étiquetée meilleures pratiques, je vais y aller et ajouter mes deux sous. Pour ce genre 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 chaud-plaque, mais le code qui l'utilise semblera beaucoup plus propre:

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

contre

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

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

8
Mark A. Nicolosi

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. Je n'ai pas de VS2008 sous la main pour vérifier, cependant. Dans tous les cas, si Products implémente IEnumerable (comme cela semble être le cas, il est utilisé dans une instruction foreach), je le renverrais directement.

7
petr k.

J'aurais utilisé la version 2 du code dans ce cas. Puisque vous avez la liste complète des produits disponibles et que c'est ce que le "consommateur" de cette méthode appelait, 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 de l'information suivante est utilisée à la demande, il serait alors avantageux d'utiliser le retour de rendement, qui garantira que la commande d'exécution sera retournée à l'appelant lorsque une unité d'information est disponible.

Voici quelques exemples d'utilisation du rendement:

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

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

5

Renvoyer 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 (rendement) à partir du moment où vous pensez que vous n'aurez probablement pas à itérer jusqu'à la fin de la liste, ou lorsqu'il n'a 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 qu'il s'agisse d'un exemple artificiel, et qu'il existe probablement de meilleures façons de le réaliser. Fondamentalement, si vous savez à l'avance que toute la liste devra être calculée, il suffit de le faire dès le départ. Si vous pensez que ce ne sera pas le cas, envisagez d'utiliser la version itérateur.

3
recursive

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 que la phrase clé de retour de rendement est utilisée, CLR implémente un motif Enumerator dans ce morceau de code. Ce type d'implémentation aide le développeur de tous les types de plomberie que nous aurions autrement dû faire en l'absence du mot clé.

Supposons que si le développeur filtre une collection, parcourt 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 .

1
Vikram