C# Language
Rendement
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
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 unwhile 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
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é
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):
- Le compilateur C # génère une classe implémentant
IEnumerable<BigInteger>
etIEnumerator<BigInteger>
(<Fibonacci>d__0
dans ildasm). - 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.
- Le code le plus intéressant se trouve dans la méthode
bool IEnumerator.MoveNext()
. Fondamentalement, ce queMoveNext()
fait:- Restaure l'état actuel. Les variables comme
prev
etcurrent
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 à layield return
. - Exécute le code jusqu'au prochain
yield return
yield break
ouyield 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 invocationMoveNext
. - Pour la méthode
yield break
/}
méthode renvoie simplementfalse
ce qui signifie que l'itération est effectuée.
- Restaure l'état actuel. Les variables comme
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;
}