Utilisation pratique du mot clé «rendement» en C # [fermé]

76

Après presque 4 ans d'expérience, je n'ai pas vu de code dans lequel le mot clé rendement est utilisé. Quelqu'un peut-il me montrer une utilisation pratique (accompagnée d'une explication) de ce mot clé et, dans l'affirmative, n'y a-t-il pas d'autres moyens plus faciles de remplir ce qu'il peut faire?

Saeed Neamati
la source
9
Tout (ou du moins la plupart) de LINQ est implémenté en utilisant le rendement. Le framework Unity3D a également trouvé un bon usage pour celui-ci - il est utilisé pour suspendre les fonctions (sur les instructions de rendement) et le reprendre plus tard en utilisant l'état dans IEnumerable.
Dani
2
Cela ne devrait-il pas être déplacé vers StackOverflow?
Danny Varod
4
@Danny - Ce n'est pas approprié pour Stack Overflow, car la question ne demande pas de résoudre un problème spécifique, mais yieldplutôt de savoir ce qui peut être utilisé en général.
ChrisF
9
Pour de vrai? Je ne peux pas penser à une seule application où je ne l'ai pas utilisée.
Aaronaught

Réponses:

107

Efficacité

Le yieldmot clé crée effectivement une énumération paresseuse sur les éléments de collection qui peuvent être beaucoup plus efficaces. Par exemple, si votre foreachboucle effectue une itération sur les 5 premiers éléments de 1 million d'éléments, tout est yieldrenvoyé et vous n'avez pas constitué une collection d'un million d'éléments en interne. De même , vous voulez utiliser yieldavec des IEnumerable<T>valeurs de retour dans vos propres scénarios de programmation pour atteindre les mêmes gains d' efficience.

Exemple d'efficacité gagnée dans un certain scénario

Pas une méthode itérateur, 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.

Simplifier certains scénarios de programmation

Dans un autre cas, il est plus facile de programmer certaines sortes de tri et de fusion de listes car il vous suffit de yieldremettre les éléments dans l'ordre souhaité plutôt que de les trier dans une collection intermédiaire et de les échanger. 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 d'éléments contigus, une fusion sans aucune collection intermédiaire.

Plus d'informations

Le yieldmot - clé ne peut être utilisé dans le contexte d'un procédé d'itération (ayant un type de retour IEnumerable, IEnumerator, IEnumerable<T>, ou IEnumerator<T>.) Et il existe une relation particulière avec foreach. Les itérateurs sont des méthodes spéciales. La documentation sur le rendement MSDN et la documentation sur les itérateurs contiennent de nombreuses informations et explications intéressantes sur les concepts. Assurez-vous de le corréler avec le foreachmot - clé en lisant à ce sujet également, afin de 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 radicalement de celui généré pour une méthode régulière (sans itérateur). Cet article (Qu'est-ce que le mot-clé Yield génère-t-il vraiment?) Fournit ce type d'informations.

John K
la source
2
Ils sont particulièrement utiles pour les algorithmes qui prennent une séquence (éventuellement longue) et en génèrent une autre pour laquelle le mappage n'est pas un à un. Un exemple de ceci est le découpage de polygone; une arête particulière ne peut générer beaucoup d’arêtes, voire aucune. Les itérateurs rendent cela extrêmement facile à exprimer, et le rendement est l’un des meilleurs moyens de les écrire.
Donal Fellows le
+1 Bien meilleure réponse que j'avais écrite. Maintenant, j'ai aussi appris que le rendement est bon pour de meilleures performances.
Jan_V
3
Il était une fois, j'avais l'habitude de créer des paquets pour un protocole réseau binaire. Cela semblait être le choix le plus naturel en C #.
György Andrasek
4
L' database.Customers.Count()énumération de l' énumération de tous les clients ne nécessite- t-elle pas le code le plus efficace pour parcourir chaque élément?
Stephen
5
Appelez-moi anal, mais c'est concaténation, pas fusion. (Et linq a déjà une méthode Concat.)
OldFart
4

Il y a quelque temps, j'ai eu 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 à Dictionary<T>ou à d'autres types de collections.

Voici ma solution en utilisant le yieldmot-clé:

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 forboucle sur ma collection pour trouver l'index. My Button a un identifiant qui est également utilisé comme index IndexerList<T>pour éviter tout identifiant ou index redondant - c’est ce que j’aime! L'index / Id peut être un nombre arbitraire.

Wernfried Domscheit
la source
2

Un exemple pratique peut être trouvé ici:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

L'utilisation du rendement par rapport au code standard présente plusieurs avantages:

  • Si l'itérateur est utilisé pour créer une liste, vous pouvez générer le retour et l'appelant peut décider s'il souhaite ou non obtenir ce résultat dans une liste.
  • L’appelant peut également décider d’annuler l’itération pour une raison différente 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 un code presque identique dans les deux cas.

Jalayn
la source
1

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 chaque jour de la semaine avec une heure de pause pour le déjeuner à 12h30. Avec cette connaissance, la fonction AscendingShifts () générera des objets de travail par quart de travail entre les dates fournies. Pour lister tous les quarts de travail de Bob entre le 1er janvier et le 1er février de cette année, vous l'utiliseriez comme ceci:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

La classe ne parcourt pas vraiment une collection. Cependant, les décalages entre deux dates peuvent être considérés comme une collection. L' yieldopérateur permet de parcourir cette collection imaginée sans créer la collection elle-même.

Fourmi
la source
1

J'ai une petite couche de données de base de données qui a une commandclasse dans laquelle vous définissez le texte de la commande SQL, le type de commande et renvoyez un IEnumerable de 'paramètres de commande'.

En gros, l’idée est d’avoir des commandes CLR tapées au lieu de saisir manuellement les SqlCommandpropriétés et les paramètres tout le temps.

Donc, il y a 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 commandclasse a les propriétés Ageet Name.

Ensuite, vous pouvez créer un commandobjet rempli de ses propriétés et le transmettre à une dbinterface qui effectue l'appel de commande.

Globalement, il est très facile de travailler avec des commandes SQL et de les conserver typées.

John
la source
1

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 TM:

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 MarkChecksumméthode, par exemple, ressemble à ceci. Et il y a yieldaussi:

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 telles que Sum () dans une méthode d'énumération, car elles déclenchent un processus d'énumération distinct.

Yegor
la source
1

Elastic Search .NET exemple. Repo a un excellent exemple d’utilisation de yield return de la partition d'une collection en plusieurs collections d'une taille donné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);
        }
    }
pholly
la source
0

En développant la réponse de Jan_V, je viens de citer un cas concret qui le concerne:

J'avais besoin d'utiliser les versions Kernel32 de FindFirstFile / FindNextFile. Vous obtenez un descripteur du premier appel et le transmettez à tous les appels suivants. Enveloppez cela dans un énumérateur et vous obtiendrez quelque chose que vous pouvez utiliser directement avec foreach.

Loren Pechtel
la source