Après presque 4 ans d'expérience, je n'ai pas vu de code où yield mot clé est utilisé. Quelqu'un peut-il me montrer une utilisation pratique (avec explication) de ce mot-clé, et si oui, n'y a-t-il pas d'autres moyens plus faciles de remplir ce qu'il peut faire?
Le mot clé yield
crée efficacement une énumération paresseuse des éléments de collection qui peut être beaucoup plus efficace. Par exemple, si votre boucle foreach
itère uniquement sur les 5 premiers éléments de 1 million d'éléments, alors c'est tout yield
renvoie, et vous n'avez pas d'abord créé une collection de 1 million d'éléments en interne. De même, vous voudrez utiliser yield
avec IEnumerable<T>
renvoie des valeurs dans vos propres scénarios de programmation pour atteindre les mêmes efficacités.
Exemple d'efficacité gagnée dans un certain scénario
Pas une méthode itérative, utilisation potentiellement inefficace d'une grande collection,
(La collection intermédiaire est construite avec beaucoup d'articles)
// Method returns all million items before anything can loop over them.
List<object> GetAllItems() {
List<object> millionCustomers;
database.LoadMillionCustomerRecords(millionCustomers);
return millionCustomers;
}
// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems()) {
num++;
if (num == 5)
break;
}
// Note: One million items returned, but only 5 used.
Version itérateur, efficace
(Aucune collection intermédiaire n'est construite)
// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
for (int i; i < database.Customers.Count(); ++i)
yield return database.Customers[i];
}
// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems()) {
num++;
if (num == 5)
break;
}
// Note: Only 5 items were yielded and used out of the million.
Dans un autre cas, cela rend certains types de tri et de fusion de listes plus faciles à programmer car vous venez de yield
éléments dans l'ordre souhaité plutôt que de les trier dans une collection intermédiaire et de les échanger dedans. Il existe de nombreux scénarios de ce type.
Un seul exemple est la fusion de deux listes:
IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
foreach(var o in list1)
yield return o;
foreach(var o in list2)
yield return o;
}
Cette méthode renvoie une liste contiguë d'éléments, en fait une fusion sans collecte intermédiaire nécessaire.
Le mot clé yield
ne peut être utilisé que dans le contexte d'une méthode itérateur (ayant un type de retour IEnumerable
, IEnumerator
, IEnumerable<T>
, ou IEnumerator<T>
.) et il existe une relation spéciale avec foreach
. Les itérateurs sont des méthodes spéciales. documentation de rendement MSDN et documentation de l'itérateur contient de nombreuses informations intéressantes et une explication des concepts. Assurez-vous de le corréler avec le mot clé foreach
en lisant également à ce sujet, pour compléter votre compréhension des itérateurs.
Pour savoir comment les itérateurs atteignent leur efficacité, le secret réside dans le code IL généré par le compilateur C #. L'IL généré pour une méthode itérateur diffère considérablement de celui généré pour une méthode régulière (non itérateur). Cet article (Qu'est-ce que le mot-clé de rendement génère vraiment?) fournit ce genre d'informations.
Il y a quelque temps, j'avais un exemple pratique, supposons que vous ayez une situation comme celle-ci:
List<Button> buttons = new List<Button>();
void AddButtons()
{
for ( int i = 0; i <= 10; i++ ) {
var button = new Button();
buttons.Add(button);
button.Click += (sender, e) =>
MessageBox.Show(String.Format("You clicked button number {0}", ???));
}
}
L'objet bouton ne connaît pas sa propre position dans la collection. La même limitation s'applique pour Dictionary<T>
ou d'autres types de collections.
Voici ma solution en utilisant le mot clé yield
:
interface IHasId { int Id { get; set; } }
class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
List<T> elements = new List<T>();
new public void Clear() { elements.Clear(); }
new public void Add(T element) { elements.Add(element); }
new public int Count { get { return elements.Count; } }
new public IEnumerator<T> GetEnumerator()
{
foreach ( T c in elements )
yield return c;
}
new public T this[int index]
{
get
{
foreach ( T c in elements ) {
if ( (int)c.Id == index )
return c;
}
return default(T);
}
}
}
Et c'est comme ça que je l'utilise:
class ButtonWithId: Button, IHasId
{
public int Id { get; private set; }
public ButtonWithId(int id) { this.Id = id; }
}
IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
for ( int i = 10; i <= 20; i++ ) {
var button = new ButtonWithId(i);
buttons.Add(button);
button.Click += (sender, e) =>
MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
}
}
Je n'ai pas besoin de faire une boucle for
sur ma collection pour trouver l'index. Mon bouton a un identifiant et est également utilisé comme index dans IndexerList<T>
, donc vous évitez tout ID ou index redondant - c'est ce que j'aime! L'index/Id peut être un nombre arbitraire.
Un exemple pratique peut être trouvé ici:
http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html
Il y a plusieurs avantages à utiliser le rendement par rapport au code standard:
Cependant, comme Jan_V l'a dit (il suffit de me battre de quelques secondes :-) vous pouvez vous en passer car en interne le compilateur produira du code presque identique dans les deux cas.
J'ai une petite couche de données db qui a une classe command
dans laquelle vous définissez le texte de la commande SQL, le type de commande et retournez un IEnumerable de 'paramètres de commande'.
Fondamentalement, l'idée est d'avoir tapé des commandes CLR au lieu de remplir manuellement les propriétés et les paramètres SqlCommand
tout le temps.
Il y a donc une fonction qui ressemble à ceci:
IEnumerable<DbParameter> GetParameters()
{
// here i do something like
yield return new DbParameter { name = "@Age", value = this.Age };
yield return new DbParameter { name = "@Name", value = this.Name };
}
La classe qui hérite de cette classe command
a les propriétés Age
et Name
.
Ensuite, vous pouvez ajouter un objet command
rempli ses propriétés et le passer à une interface db
qui fait l'appel de la commande.
Dans l'ensemble, il est très facile de travailler avec des commandes SQL et de les garder tapées.
Voici un exemple:
https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158
La classe effectue des calculs de date basés sur une semaine de travail. Je peux dire un exemple de la classe que Bob travaille de 9h30 à 17h30 tous les jours de la semaine avec une pause d'une heure pour le déjeuner à 12h30. Avec cette connaissance, la fonction AscendingShifts () produira des objets de travail décalés entre les dates fournies. Pour répertorier tous les quarts de travail de Bob entre le 1er janvier et le 1er février de cette année, vous devez l'utiliser comme ceci:
foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
Console.WriteLine(shift);
}
La classe n'itère pas vraiment sur une collection. Cependant, les décalages entre deux dates peuvent être considérés comme une collection. L'opérateur yield
permet de parcourir cette collection imaginée sans créer la collection elle-même.
Bien que le cas de fusion ait déjà été traité dans la réponse acceptée, permettez-moi de vous montrer la méthode d'extension des paramètres de rendement-fusion ™:
public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
foreach (var el in a) yield return el;
foreach (var el in b) yield return el;
}
J'utilise ceci pour construire des paquets d'un protocole réseau:
static byte[] MakeCommandPacket(string cmd)
{
return
header
.AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
.AppendAscii(cmd)
.MarkLength()
.MarkChecksum()
.ToArray();
}
La méthode MarkChecksum
, par exemple, ressemble à ceci. Et il a aussi un yield
:
public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
foreach (byte b in data)
{
yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
}
}
Mais soyez prudent lorsque vous utilisez des méthodes d'agrégation comme Sum () dans une méthode d'énumération car elles déclenchent un processus d'énumération distinct.
Le référentiel d'exemples Elastic Search .NET offre un excellent exemple d'utilisation de yield return
pour partitionner une collection en plusieurs collections avec une taille spécifiée:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
{
T[] array = null;
int count = 0;
foreach (T item in source)
{
if (array == null)
{
array = new T[size];
}
array[count] = item;
count++;
if (count == size)
{
yield return new ReadOnlyCollection<T>(array);
array = null;
count = 0;
}
}
if (array != null)
{
Array.Resize(ref array, count);
yield return new ReadOnlyCollection<T>(array);
}
}
En développant la réponse de Jan_V, je viens de frapper un cas réel lié à cela:
J'avais besoin d'utiliser les versions Kernel32 de FindFirstFile/FindNextFile. Vous obtenez une poignée dès le premier appel et la nourrissez pour tous les appels suivants. Enveloppez-le dans un énumérateur et vous obtenez quelque chose que vous pouvez utiliser directement avec foreach.