Un uso pratico della parola chiave "rendimento" in C # [chiuso]

73

Dopo quasi 4 anni di esperienza, non ho visto un codice in cui viene utilizzata la parola chiave yield . Qualcuno può mostrarmi un uso pratico (lungo la spiegazione) di questa parola chiave e, in caso affermativo, non ci sono altri modi più facili per completare ciò che può fare?

    
posta Saeed Neamati 30.07.2011 - 20:01
fonte

8 risposte

103

Efficienza

La parola chiave yield crea effettivamente un'enumerazione pigra sugli elementi di raccolta che può essere molto più efficiente. Ad esempio, se il tuo ciclo foreach esegue iterazioni solo sui primi 5 elementi di 1 milione di articoli, questo è tutto yield restituito e non hai prima creato una raccolta di 1 milione di articoli internamente. Allo stesso modo, vorrai utilizzare yield con IEnumerable<T> valori di ritorno nei tuoi scenari di programmazione per ottenere le stesse efficienze.

Esempio di efficienza acquisita in un determinato scenario

Non è un metodo iteratore, un potenziale uso inefficiente di una grande raccolta,
(la raccolta intermedia è costruita con molti elementi)

// 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. 

Versione Iterator, efficiente
(Nessuna raccolta intermedia è stata creata)

// 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.

Semplifica alcuni scenari di programmazione

In un altro caso, rende più facili da programmare alcuni tipi di ordinamento e fusione di elenchi, perché solo yield elementi indietro nell'ordine desiderato anziché ordinarli in una raccolta intermedia e scambiarli lì. Ci sono molti di questi scenari.

Un solo esempio è la fusione di due elenchi:

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;
}

Questo metodo restituisce un elenco contiguo di elementi, in pratica un'unione senza alcuna raccolta intermedia necessaria.

Altre informazioni

La parola chiave yield può essere utilizzata solo nel contesto di un metodo iteratore (con un tipo di ritorno di IEnumerable , IEnumerator , IEnumerable<T> o IEnumerator<T> .) e c'è una relazione speciale con% codice%. Gli iteratori sono metodi speciali. documentazione di rendimento MSDN e la documentazione di iterator contiene molte informazioni interessanti e spiegazioni sui concetti. Assicurati di correlarlo con la parola chiave foreach di leggendo anche su questo, per integrare la tua comprensione degli iteratori.

Per sapere come gli iteratori raggiungono la loro efficienza, il segreto è nel codice IL generato dal compilatore C #. L'IL generato per un metodo iteratore differisce drasticamente da quello generato per un metodo regolare (non iteratore). Questo articolo (Cosa fa la parola chiave di rendimento Really Generate?) fornisce quel tipo di intuizione.

    
risposta data 31.07.2011 - 05:56
fonte
4

Qualche tempo fa ho avuto un esempio pratico, supponiamo che tu abbia una situazione del genere:

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'oggetto pulsante non conosce la propria posizione nella raccolta. La stessa limitazione vale per Dictionary<T> o altri tipi di collezioni.

Ecco la mia soluzione utilizzando yield keyword:

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

Ed è così che lo uso:

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

Non devo creare un ciclo for sulla mia raccolta per trovare l'indice. My Button ha un ID e questo è anche usato come indice in IndexerList<T> , quindi eviti qualsiasi ID o indice ridondante - questo è quello che mi piace! L'indice / Id può essere un numero arbitrario.

    
risposta data 09.06.2015 - 19:33
fonte
2

Un esempio pratico può essere trovato qui:

link

Ci sono una serie di vantaggi nell'usare il rendimento rispetto al codice standard:

  • Se l'iteratore viene utilizzato per creare un elenco, puoi restituire il rendimento e il chiamante può decidere se desidera o meno quel risultato in un elenco o meno.
  • Il chiamante può anche decidere di annullare l'iterazione per un motivo che è al di fuori dell'ambito di ciò che stai facendo nell'iterazione.
  • Il codice è un po 'più breve.

Tuttavia, come diceva Jan_V (bastonami per pochi secondi :-) puoi vivere senza di esso perché internamente il compilatore produrrà codice quasi identico in entrambi i casi.

    
risposta data 30.07.2011 - 20:26
fonte
1

Ecco un esempio:

link

La classe esegue calcoli di date basati su una settimana lavorativa. Posso dire a un'istanza della classe che Bob lavora dalle 9:30 alle 17:30 ogni giorno della settimana con un'ora di pausa per il pranzo alle 12:30. Con questa conoscenza, la funzione AscendingShifts () produrrà oggetti di lavoro in movimento tra le date fornite. Per elencare tutti i turni lavorativi di Bob tra il 1 ° gennaio e il 1 ° febbraio di quest'anno, lo useresti in questo modo:

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

La classe non esegue iterazione su una raccolta. Tuttavia, i cambiamenti tra due date possono essere pensati come una raccolta. L'operatore yield rende possibile iterare su questa collezione immaginata senza creare la raccolta stessa.

    
risposta data 31.07.2011 - 12:28
fonte
1

Ho un piccolo livello dati db che ha una classe command in cui imposti il testo del comando SQL, il tipo di comando, e restituisci un IEnumerable di "parametri di comando".

Fondamentalmente l'idea è di aver digitato i comandi CLR invece di riempire manualmente SqlCommand di proprietà e parametri per tutto il tempo.

Quindi esiste una funzione simile a questa:

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 che eredita questa classe command ha le proprietà Age e Name .

Quindi puoi rinnovare un oggetto command riempito le sue proprietà e passarlo a un'interfaccia db che in realtà esegue la chiamata al comando.

Tutto sommato è davvero facile lavorare con i comandi SQL e mantenerli digitati.

    
risposta data 31.07.2011 - 12:05
fonte
1

Sebbene il caso di fusione sia già stato trattato nella risposta accettata, lascia che ti mostri il metodo di estensione params yield-merge ™:

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;
}

Lo uso per creare pacchetti di un protocollo di rete:

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();
}

Il metodo MarkChecksum , ad esempio, assomiglia a questo. E ha anche 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;
    }
}

Ma fai attenzione quando usi metodi di aggregazione come Sum () in un metodo di enumerazione quando attivano un processo di enumerazione separato.

    
risposta data 10.06.2015 - 13:15
fonte
1

Il repository di esempio Elastic Search .NET ha un ottimo esempio di utilizzo di yield return per partizionare una raccolta in più raccolte con una dimensione specificata:

link

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);
        }
    }
    
risposta data 10.09.2015 - 15:34
fonte
0

Espandendo la risposta di Jan_V, ho appena trovato un caso reale relativo ad esso:

Avevo bisogno di usare le versioni Kernel32 di FindFirstFile / FindNextFile. Si ottiene un handle dalla prima chiamata e lo si invia a tutte le chiamate successive. Avvolgi questo in un enumeratore e ottieni qualcosa che puoi utilizzare direttamente con foreach.

    
risposta data 01.02.2015 - 17:32
fonte

Leggi altre domande sui tag