web-dev-qa-db-fra.com

Une utilisation pratique du mot-clé "yield" en C #

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?

77
Saeed Neamati

Efficacité

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.

Simplifiez certains scénarios de programmation

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.

Plus d'informations

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.

109
John K

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.

4

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:

  • Si l'itérateur est utilisé pour construire une liste, vous pouvez produire le retour et l'appelant peut décider s'il veut ou non que ce résultat apparaisse dans une liste.
  • L'appelant peut également décider d'annuler l'itération pour une raison qui n'entre pas dans le cadre de ce que vous faites dans l'itération.
  • Le code est un peu plus court.

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.

2
Jalayn

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.

1
john

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.

1
Ant

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.

1
Yegor

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:

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

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);
        }
    }
1
pholly

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.

0
Loren Pechtel