Recherche…


Introduction

Lorsque vous utilisez le mot-clé yield dans une instruction, vous indiquez que la méthode, l'opérateur ou le accesseur dans lequel il apparaît est un itérateur. L'utilisation de yield pour définir un itérateur supprime la nécessité d'une classe supplémentaire explicite (la classe qui contient l'état d'une énumération) lorsque vous implémentez les modèles IEnumerable et IEnumerator pour un type de collection personnalisé.

Syntaxe

  • rendement de retour [TYPE]
  • rupture de rendement

Remarques

Indiquer le mot-clé de yield dans une méthode avec le type de retour IEnumerable , IEnumerable<T> , IEnumerator ou IEnumerator<T> indique au compilateur de générer une implémentation du type de retour ( IEnumerable ou IEnumerator ) qui exécute la boucle méthode jusqu'à chaque "rendement" pour obtenir chaque résultat.

Le mot-clé yield est utile lorsque vous souhaitez renvoyer l'élément "suivant" d'une séquence théoriquement illimitée. Il serait donc impossible de calculer l'intégralité de la séquence ou de calculer la séquence complète des valeurs avant de renvoyer une pause indésirable pour l'utilisateur. .

yield break peut également être utilisée pour terminer la séquence à tout moment.

Comme le mot clé yield nécessite un type d'interface d'itérateur en tant que type de retour, tel que IEnumerable<T> , vous ne pouvez pas l'utiliser dans une méthode asynchrone, car cela retourne un objet Task<IEnumerable<T>> .

Lectures complémentaires

Utilisation simple

Le mot-clé yield est utilisé pour définir une fonction qui renvoie un IEnumerable ou un IEnumerator (ainsi que leurs variantes génériques dérivées) dont les valeurs sont générées paresseusement au fur et à mesure qu'un utilisateur effectue une itération sur la collection renvoyée. En savoir plus sur le but dans la section remarques .

L'exemple suivant contient une instruction return return qui se trouve dans une boucle for .

public static IEnumerable<int> Count(int start, int count)
{
    for (int i = 0; i <= count; i++)
    {
        yield return start + i;
    }
}

Ensuite, vous pouvez l'appeler:

foreach (int value in Count(start: 4, count: 10))
{
    Console.WriteLine(value);
}

Sortie de console

4
5
6
...
14

Démo en direct sur .NET Fiddle

Chaque itération du corps de l'instruction foreach crée un appel à la fonction Count iterator. Chaque appel à la fonction itérateur passe à l'exécution suivante de l'instruction yield return , qui se produit lors de la prochaine itération de la boucle for .

Utilisation plus pertinente

public IEnumerable<User> SelectUsers()
{
    // Execute an SQL query on a database.
    using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
    {
        while (reader.Read())
        {
            int id = reader.GetInt32(0);
            string name = reader.GetString(1);
            yield return new User(id, name);
        }
    }
}

Bien sûr, il existe d’autres moyens d’obtenir un IEnumerable<User> partir d’une base de données SQL - cela montre simplement que vous pouvez utiliser yield pour transformer en IEnumerable<T> tout ce qui a une sémantique "séquence d’éléments". .

Résiliation anticipée

Vous pouvez étendre les fonctionnalités des méthodes de yield existantes en transmettant une ou plusieurs valeurs ou éléments pouvant définir une condition de terminaison dans la fonction en appelant une yield break pour empêcher l'exécution de la boucle interne.

public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
    int curr = start;

    while (true)
    {
        if (earlyTerminationSet.Contains(curr))
        {
            // we've hit one of the ending values
            yield break;
        }

        yield return curr;

        if (curr == Int32.MaxValue)
        {
            // don't overflow if we get all the way to the end; just stop
            yield break;
        }

        curr++;
    }
}

La méthode ci-dessus effectuerait une itération à partir d’une position de start donnée jusqu’à ce qu’une des valeurs earlyTerminationSet dans le earlyTerminationSet soit rencontrée.

// Iterate from a starting point until you encounter any elements defined as 
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
    // This will write out the results from 1 until 7 (which will trigger terminating)
    Console.WriteLine(x);
}

Sortie:

1
2
3
4
5
6

Démo en direct sur .NET Fiddle

Vérification correcte des arguments

Une méthode itérateur n'est pas exécutée tant que la valeur de retour n'est pas énumérée. Il est donc avantageux d'affirmer des conditions préalables en dehors de l'itérateur.

public static IEnumerable<int> Count(int start, int count)
{
    // The exception will throw when the method is called, not when the result is iterated
    if (count < 0)
        throw new ArgumentOutOfRangeException(nameof(count));

    return CountCore(start, count);
}

private static IEnumerable<int> CountCore(int start, int count)
{
    // If the exception was thrown here it would be raised during the first MoveNext()
    // call on the IEnumerator, potentially at a point in the code far away from where
    // an incorrect value was passed.
    for (int i = 0; i < count; i++)
    {
        yield return start + i;
    }
}

Code côté appelant (utilisation):

// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
    Console.WriteLine(x);
}

Sortie:

1
2
3
4
5
6
7
8
9
dix

Démo en direct sur .NET Fiddle

Lorsqu'une méthode utilise un yield pour générer un énumérable, le compilateur crée une machine d'état qui, une fois itérée, exécute du code jusqu'à un yield . Il retourne ensuite l'élément produit et enregistre son état.

Cela signifie que vous ne découvrirez pas d’arguments non valides ( null passant, etc.) lorsque vous appelez la méthode pour la première fois (car cela crée la machine d’état), uniquement lorsque vous essayez d’accéder au premier élément méthode est exécuté par la machine d'état). En l'encapsulant dans une méthode normale qui vérifie d'abord les arguments, vous pouvez les vérifier lorsque la méthode est appelée. Ceci est un exemple d'échec rapide.

Lorsque vous utilisez C # 7+, la fonction CountCore peut être facilement cachée dans la fonction Count tant que fonction locale . Voir exemple ici .

Renvoie un autre Enumerable dans une méthode renvoyant Enumerable

public IEnumerable<int> F1()
{
    for (int i = 0; i < 3; i++)
        yield return i;

    //return F2(); // Compile Error!!
    foreach (var element in F2())
        yield return element;
}

public int[] F2()
{
    return new[] { 3, 4, 5 };
}

Évaluation paresseuse

Ce n'est que lorsque l'instruction foreach passe à l'élément suivant que le bloc itérateur est évalué jusqu'à la déclaration de yield suivante.

Prenons l'exemple suivant:

private IEnumerable<int> Integers()
{
    var i = 0;
    while(true)
    {
        Console.WriteLine("Inside iterator: " + i);
        yield return i;
        i++;
    }
}

private void PrintNumbers()
{
    var numbers = Integers().Take(3);
    Console.WriteLine("Starting iteration");

    foreach(var number in numbers)
    {
        Console.WriteLine("Inside foreach: " + number);
    }
}

Cela va sortir:

Début de l'itération
Itérateur interne: 0
Intérieur de la salle: 0
Itérateur interne: 1
Inside foreach: 1
Itérateur intérieur: 2
Intérieur de la salle: 2

Voir la démo

En conséquence:

  • "Lancement de l'itération" est imprimé en premier, même si la méthode de l'itérateur a été appelée avant l'impression par la ligne Integers().Take(3); ne démarre pas réellement l'itération (aucun appel à IEnumerator.MoveNext() n'a été effectué)
  • Les lignes imprimant sur la console alternent entre celle qui se trouve dans la méthode de l'itérateur et celle de la méthode foreach , plutôt que celles de la méthode de l'itérateur évaluant d'abord
  • Ce programme se termine en raison de la méthode .Take() , même si la méthode iterator a un while true dont elle ne sort jamais.

Essayez ... enfin

Si une méthode itérative a un rendement dans un try...finally , le IEnumerator retourné exécutera l'instruction finally lorsque Dispose sera appelée, tant que le point d'évaluation actuel se trouve dans le bloc try .

Vu la fonction:

private IEnumerable<int> Numbers()
{
    yield return 1;
    try
    {
        yield return 2;
        yield return 3;
    }
    finally
    {
        Console.WriteLine("Finally executed");
    }
}

En appelant:

private void DisposeOutsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Ensuite, il imprime:

1

Voir la démo

En appelant:

private void DisposeInsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Ensuite, il imprime:

1
2
Enfin exécuté

Voir la démo

Utiliser les rendements pour créer un IEnumerator lors de l'implémentation de IEnumerable

L'interface IEnumerable<T> possède une méthode unique, GetEnumerator() , qui renvoie un IEnumerator<T> .

Alors que le mot-clé yield peut être utilisé pour créer directement un IEnumerable<T> , il peut également être utilisé exactement de la même manière pour créer un IEnumerator<T> . La seule chose qui change est le type de retour de la méthode.

Cela peut être utile si nous voulons créer notre propre classe qui implémente IEnumerable<T> :

public class PrintingEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _wrapped;

    public PrintingEnumerable(IEnumerable<T> wrapped)
    {
        _wrapped = wrapped;
    }

    // This method returns an IEnumerator<T>, rather than an IEnumerable<T>
    // But the yield syntax and usage is identical.
    public IEnumerator<T> GetEnumerator()
    {
        foreach(var item in _wrapped)
        {
            Console.WriteLine("Yielding: " + item);
            yield return item;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

(Notez que cet exemple particulier est simplement illustratif et pourrait être implémenté plus proprement avec une seule méthode itérateur renvoyant un IEnumerable<T> .)

Évaluation avide

Le mot-clé de yield permet une évaluation paresseuse de la collection. Le chargement forcé de toute la collection en mémoire est appelé évaluation rapide .

Le code suivant montre ceci:

IEnumerable<int> myMethod()
{
    for(int i=0; i <= 8675309; i++)
    {
        yield return i;
    }
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();

L'appel de ToList , ToDictionary ou ToArray force l'évaluation immédiate de l'énumération, en récupérant tous les éléments dans une collection.

Exemple d'évaluation paresseuse: numéros de Fibonacci

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics

namespace ConsoleApplication33
{
    class Program
    {
        private static IEnumerable<BigInteger> Fibonacci()
        {
            BigInteger prev = 0;
            BigInteger current = 1;
            while (true)
            {
                yield return current;
                var next = prev + current;
                prev = current;
                current = next;
            }
        }

        static void Main()
        {
            // print Fibonacci numbers from 10001 to 10010
            var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
            Console.WriteLine(string.Join(Environment.NewLine, numbers));
        }
    }
}

Comment ça marche sous le capot (je recommande de décompiler le fichier .exe résultant dans l'outil IL Disaambler):

  1. Le compilateur C # génère une classe implémentant IEnumerable<BigInteger> et IEnumerator<BigInteger> ( <Fibonacci>d__0 dans ildasm).
  2. Cette classe implémente une machine à états. L'état se compose de la position actuelle dans la méthode et des valeurs des variables locales.
  3. Le code le plus intéressant se trouve dans la méthode bool IEnumerator.MoveNext() . Fondamentalement, ce que MoveNext() fait:
    • Restaure l'état actuel. Les variables comme prev et current deviennent des champs dans notre classe ( <current>5__2 et <prev>5__1 in ildasm). Dans notre méthode, nous avons deux positions ( <>1__state ): la première à l'accolade ouvrante, la deuxième à la yield return .
    • Exécute le code jusqu'au prochain yield return yield break ou yield break / } .
    • Pour les yield return valeur résultante est enregistrée, ainsi la propriété Current peut la renvoyer. true est retourné. À ce stade, l'état actuel est à nouveau enregistré pour la prochaine invocation MoveNext .
    • Pour la méthode yield break / } méthode renvoie simplement false ce qui signifie que l'itération est effectuée.

Notez également que ce nombre 10001 est long de 468 octets. La machine d'état enregistre uniquement current variables current et prev tant que champs. Alors que si vous souhaitez enregistrer tous les nombres dans la séquence du premier au dixième, la taille de la mémoire consommée sera supérieure à 4 mégaoctets. L'évaluation paresseuse, si elle est utilisée correctement, peut réduire l'encombrement de la mémoire dans certains cas.

La différence entre break et break

Utiliser la yield break par opposition à la break pourrait ne pas être aussi évidente qu'on pourrait le penser. Il y a beaucoup de mauvais exemples sur Internet où l'utilisation des deux est interchangeable et ne démontre pas vraiment la différence.

La partie déroutante est que les deux mots-clés (ou phrases-clés) n'ont de sens que dans les boucles ( foreach , while ...) Alors, quand choisir l'un sur l'autre?

Il est important de réaliser qu'une fois que vous utilisez le mot-clé yield dans une méthode, vous transformez la méthode en itérateur . Le seul but de cette méthode est alors d’itérer sur une collection finie ou infinie et de produire (produire) ses éléments. Une fois l'objectif atteint, il n'y a aucune raison de continuer l'exécution de la méthode. Parfois, cela se produit naturellement avec le dernier crochet de fermeture de la méthode } . Mais parfois, vous voulez mettre fin à la méthode prématurément. Dans une méthode normale (non itérative), vous utiliseriez le mot-clé return . Mais vous ne pouvez pas utiliser return dans un itérateur, vous devez utiliser yield break . En d' autres termes, la yield break pour un itérateur est le même que le return d'une méthode normalisée. Alors que l'instruction break fait que terminer la boucle la plus proche.

Voyons quelques exemples:

    /// <summary>
    /// Yields numbers from 0 to 9
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
    public static IEnumerable<int> YieldBreak()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Indicates that the iteration has ended, everything 
                // from this line on will be ignored
                yield break;
            }
        }
        yield return 10; // This will never get executed
    }
    /// <summary>
    /// Yields numbers from 0 to 10
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
    public static IEnumerable<int> Break()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Terminates just the loop
                break;
            }
        }
        // Execution continues
        yield return 10;
    }


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow