Question À quoi sert le mot-clé yield en C #?


dans le Comment puis-je exposer seulement un fragment de IList <> question l'une des réponses avait l'extrait de code suivant:

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

Que fait le mot clé yield? Je l'ai vu référencé à quelques endroits, et une autre question, mais je n'ai pas tout à fait compris ce qu'il fait réellement. J'ai l'habitude de penser au rendement dans le sens d'un fil qui cède à un autre, mais cela ne semble pas pertinent ici.


657
2017-09-02 13:15


origine


Réponses:


Le mot-clé yield fait beaucoup ici. La fonction renvoie un objet qui implémente l'interface IEnumerable. Si une fonction d'appel commence à foreacher sur cet objet, la fonction est appelée à nouveau jusqu'à ce qu'elle "cède". C'est du sucre syntaxique introduit en C # 2.0. Dans les versions antérieures, vous deviez créer vos propres objets IEnumerable et IEnumerator pour faire ce genre de choses.

La façon la plus simple de comprendre le code est de saisir un exemple, de définir des points d'arrêt et de voir ce qui se passe.

Essayez de passer par là, par exemple:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Lorsque vous parcourez l'exemple, le premier appel à Integers () renvoie 1. Le deuxième appel renvoie 2 et la ligne "yield return 1" n'est pas exécutée à nouveau.

Voici un exemple réel

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

580
2017-09-02 13:23



Itération. Il crée une machine d'état "sous les couvertures" qui se souvient où vous étiez sur chaque cycle supplémentaire de la fonction et ramasse à partir de là.


323
2017-09-02 13:17



Le rendement a deux grandes utilisations,

  1. Il permet de fournir une itération personnalisée sans créer de collections temporaires.

  2. Cela aide à faire une itération avec état. enter image description here

Afin d'expliquer plus haut deux points de manière plus démonstrative, j'ai créé une vidéo simple que vous pouvez regarder ici


145
2018-04-12 17:31



Récemment, Raymond Chen a également publié une intéressante série d'articles sur le mot-clé yield.

Alors qu'il est nominalement utilisé pour implémenter facilement un modèle d'itérateur, mais peut être généralisé dans une machine d'état. Il ne sert à rien de citer Raymond, la dernière partie renvoie également à d'autres usages (mais l'exemple du blog d'Entin est particulièrement bon, montrant comment écrire du code asynchrone sécurisé).


124
2017-09-02 13:27



À première vue, le rendement du rendement est un sucre .NET pour retourner un IEnumerable.

Sans rendement, tous les éléments de la collection sont créés à la fois:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Même code en utilisant le rendement, il renvoie un article par article:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

L'avantage de l'utilisation du rendement est que si la fonction consommant vos données nécessite simplement le premier élément de la collection, le reste des éléments ne sera pas créé.

L'opérateur de rendement permet la création d'articles tels qu'ils sont demandés. C'est une bonne raison de l'utiliser.


46
2018-01-17 14:23



yield return est utilisé avec les enquêteurs. Sur chaque déclaration d'appel de rendement, le contrôle est retourné à l'appelant, mais il s'assure que l'état de l'appelé est maintenu. Pour cette raison, lorsque l'appelant énumère l'élément suivant, il continue l'exécution dans la méthode callee à partir de l'instruction immédiatement après le yielddéclaration.

Essayons de comprendre cela avec un exemple. Dans cet exemple, correspondant à chaque ligne j'ai mentionné l'ordre dans lequel l'exécution circule.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

En outre, l'état est maintenu pour chaque énumération. Supposons que j'ai un autre appel à Fibs() méthode alors l'état sera réinitialisé pour cela.


29
2018-02-25 17:15



Intuitivement, le mot-clé renvoie une valeur de la fonction sans la quitter, c'est-à-dire dans votre exemple de code, il retourne le courant item valeur, puis reprend la boucle. Plus formellement, il est utilisé par le compilateur pour générer du code pour un itérateur. Les itérateurs sont des fonctions qui retournent IEnumerable objets. le MSDN a plusieurs des articles à propos d'eux.


26
2017-09-02 13:19



Une implémentation de liste ou de tableau charge immédiatement tous les éléments alors que l'implémentation de rendement fournit une solution d'exécution différée.

En pratique, il est souvent souhaitable d'effectuer le minimum de travail nécessaire afin de réduire la consommation de ressources d'une application.

Par exemple, nous pouvons avoir une application qui traite des millions d'enregistrements à partir d'une base de données. Les avantages suivants peuvent être obtenus lorsque nous utilisons IEnumerable dans un modèle à extraction différée:

  • Évolutivité, fiabilité et prévisibilité sont susceptibles de s'améliorer puisque le nombre d'enregistrements n'affecte pas de manière significative les besoins en ressources de l'application.
  • Performance et réactivité sont susceptibles de s'améliorer puisque le traitement peut commencer immédiatement au lieu d'attendre que la totalité de la collection soit chargée en premier.
  • Récupérabilité et utilisation sont susceptibles de s'améliorer puisque l'application peut être arrêtée, démarrée, interrompue ou échouée. Seuls les éléments en cours seront perdus par rapport à la récupération préalable de toutes les données où seule une partie des résultats a été réellement utilisée.
  • Traitement continu est possible dans les environnements où des flux de charge de travail constants sont ajoutés.

Voici une comparaison entre construire une collection d'abord comme une liste par rapport à l'utilisation de rendement.

Exemple de liste

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Sortie de la console
ContactListStore: Création de contact 1
ContactListStore: Création du contact 2
ContactListStore: Création du contact 3
Prêt à itérer à travers la collection.

Note: La collection entière a été chargée en mémoire sans même demander un seul article dans la liste

Exemple de rendement

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Sortie de la console
Prêt à itérer à travers la collection.

Remarque: La collection n'a pas été exécutée du tout. Cela est dû à la nature "exécution différée" de IEnumerable. Construire un élément ne se produira que lorsque c'est vraiment nécessaire.

Appelons à nouveau la collection et inversons le comportement lorsque nous récupérons le premier contact de la collection.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Sortie de la console
Prêt à parcourir la collection
ContactYieldStore: Création du contact 1
Bonjour bob

Agréable! Seul le premier contact a été construit lorsque le client a "retiré" l'objet de la collection.


18
2017-12-04 19:52



Voici un moyen simple de comprendre le concept: L'idée de base est, si vous voulez une collection que vous pouvez utiliser "foreach"mais la collecte des éléments dans la collection est coûteuse pour une raison quelconque (comme les interroger depuis une base de données), ET vous n'aurez souvent pas besoin de toute la collection, alors vous créez une fonction qui construit la collection un élément à la fois et le rapporte au consommateur (qui peut alors terminer l'effort de collecte au début).

Pense-y de cette façon: Vous allez au comptoir de viande et vous voulez acheter une livre de jambon en tranches. Le boucher prend un jambon de 10 livres à l'arrière, le met sur la machine à trancher, coupe le tout, puis ramène le tas de tranches et en mesure une livre. (Ancien chemin). Avec yield, le boucher amène la machine à trancher sur le comptoir, et commence à trancher et «céder» chaque tranche sur la balance jusqu'à ce qu'elle mesure 1 livre, puis l'enveloppe pour vous et vous avez terminé. La vieille manière peut être meilleure pour le boucher (lui permet d'organiser ses machines comme il aime), mais la nouvelle manière est clairement plus efficace dans la plupart des cas pour le consommateur.


13
2017-09-01 13:43



le yield mot-clé vous permet de créer un IEnumerable<T> sous la forme sur un bloc d'itération. Ce bloc d'itération supporte exécution différée et si vous n'êtes pas familier avec le concept, il peut sembler presque magique. Cependant, à la fin de la journée, c'est juste du code qui s'exécute sans trucs bizarres.

Un bloc d'itérateur peut être décrit comme du sucre syntaxique où le compilateur génère une machine d'état qui garde trace de la progression de l'énumération de l'énumérable. Pour énumérer un énumérable, vous utilisez souvent un foreach boucle. Cependant, un foreach la boucle est aussi du sucre syntaxique. Donc, vous êtes deux abstractions retirées du code réel, c'est pourquoi il peut être difficile de comprendre comment tout cela fonctionne.

Supposons que vous ayez un bloc d'itération très simple:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

Les vrais blocs de l'itérateur ont souvent des conditions et des boucles, mais quand vous vérifiez les conditions et que vous déroulez les boucles, elles finissent yield déclarations imbriquées avec d'autres codes.

Pour énumérer le bloc de l'itérateur foreach la boucle est utilisée:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Voici la sortie (pas de surprises ici):

Commencer
1
Après 1
2
Après 2
42
Fin

Comme indiqué ci-dessus foreach est le sucre syntaxique:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Pour tenter de le démêler, j'ai créé un diagramme de séquence avec les abstractions supprimées:

C# iterator block sequence diagram

La machine d'état générée par le compilateur implémente aussi l'énumérateur mais pour rendre le diagramme plus clair, je les ai montrés comme des instances séparées. (Lorsque la machine d'état est énumérée à partir d'un autre thread, vous obtenez des instances séparées, mais ce détail n'est pas important ici.)

Chaque fois que vous appelez votre bloc d'itérateur, une nouvelle instance de la machine d'état est créée. Cependant, aucun de votre code dans le bloc itérateur n'est exécuté avant enumerator.MoveNext() s'exécute pour la première fois. C'est ainsi que l'exécution différée fonctionne. Voici un exemple (plutôt stupide):

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

À ce stade, l'itérateur n'a pas été exécuté. le Where clause crée une nouvelle IEnumerable<T>qui enveloppe le IEnumerable<T> retourné par IteratorBlock mais cet énumérable doit encore être énuméré. Cela arrive quand vous exécutez un foreach boucle:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Si vous énumérez deux fois le nombre énumérable, une nouvelle instance de la machine d'état est créée à chaque fois et votre bloc d'itération exécute deux fois le même code.

Notez que les méthodes LINQ aiment ToList(), ToArray(), First(), Count() etc. utilisera un foreach boucle pour énumérer l'énumérable. Par exemple ToList() va énumérer tous les éléments de l'énumérable et les stocker dans une liste. Vous pouvez maintenant accéder à la liste pour obtenir tous les éléments de l'énumérable sans que le bloc itérateur ne s'exécute à nouveau. Il y a un compromis entre utiliser CPU pour produire les éléments de l'énumérable plusieurs fois et la mémoire pour stocker les éléments de l'énumération pour y accéder plusieurs fois en utilisant des méthodes comme ToList().


9
2018-06-28 11:54



Le mot-clé C # yield, pour simplifier, permet de nombreux appels à un corps de code, appelé itérateur, qui sait comment retourner avant qu'il ne soit terminé et qui, lorsqu'il est appelé à nouveau, continue là où il l'a laissé. deviennent transparents pour chaque élément dans une séquence que l'itérateur renvoie dans les appels successifs.

En JavaScript, le même concept est appelé Generators.


8
2018-01-14 22:49