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


76

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 (insieme alla spiegazione) di questa parola chiave e, in tal caso, non ci sono altri modi più semplici per completare ciò che può fare?


9
Tutto (o almeno la maggior parte) di LINQ è implementato usando il rendimento. Anche il framework Unity3D ha trovato un buon uso per esso - è usato per mettere in pausa le funzioni (sulle dichiarazioni di rendimento) e riprenderlo in seguito usando lo stato in IEnumerable.
Dani,

2
Non dovrebbe essere spostato su StackOverflow?
Danny Varod,

4
@Danny - Non è adatto per Stack Overflow, in quanto la domanda non è quella di risolvere un problema specifico, ma chiedere cosa yieldpuò essere utilizzato in generale.
ChrisF

9
Davvero? Non riesco a pensare a un'unica app in cui non l' ho utilizzata.
Aaronaught,

Risposte:


107

Efficienza

La yieldparola chiave crea effettivamente un elenco pigro sugli elementi della raccolta che può essere molto più efficiente. Ad esempio, se il tuo foreachciclo scorre solo sui primi 5 articoli di 1 milione di articoli, allora sono tutti yieldritorni e non hai creato prima una raccolta di 1 milione di articoli internamente. Allo stesso modo si vuole utilizzare yieldcon i IEnumerable<T>valori di ritorno nei propri scenari di programmazione per ottenere gli stessi rendimenti.

Esempio di efficienza acquisita in un determinato scenario

Non un metodo iteratore, 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
(non è stata creata alcuna raccolta intermedia)

// 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, semplifica la programmazione di alcuni tipi di ordinamento e fusione di elenchi perché ti basta yieldriportare gli elementi nell'ordine desiderato anziché ordinarli in una raccolta intermedia e scambiarli lì. Esistono molti scenari simili.

Solo un esempio è l'unione 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 effetti un'unione senza la raccolta intermedia necessaria.

Ulteriori informazioni

La yieldparola chiave può essere utilizzato solo in contesto di un metodo iterator (avente un tipo di ritorno IEnumerable, IEnumerator, IEnumerable<T>, o IEnumerator<T>.) E v'è un rapporto speciale con foreach. Gli iteratori sono metodi speciali. La documentazione sulla resa MSDN e la documentazione dell'iteratore contengono molte informazioni e spiegazioni interessanti sui concetti. Assicurati di correlarlo con la foreachparola chiave leggendo anche su di esso, 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 normale (non iteratore). Questo articolo (Cosa genera realmente la parola chiave di rendimento?) Fornisce questo tipo di approfondimento.


2
Sono particolarmente utili per gli algoritmi che accettano una sequenza (possibilmente lunga) e ne generano un'altra in cui la mappatura non è una a una. Un esempio di questo è il ritaglio poligonale; qualsiasi bordo particolare può generare molti o addirittura nessun bordo una volta tagliato. Gli iteratori lo rendono estremamente più facile da esprimere, e cedere è uno dei modi migliori per scriverli.
Donal Fellows,

+1 Risposta molto migliore come avevo scritto. Ora ho anche imparato che la resa è buona per prestazioni migliori.
Jan_V

3
C'era una volta, ho usato la resa per creare pacchetti per un protocollo di rete binario. Sembrava la scelta più naturale in C #.
György Andrasek,

4
L' database.Customers.Count()enumerazione dell'intera enumerazione dei clienti non viene richiesta, richiedendo quindi il codice più efficiente per passare attraverso ogni articolo?
Stephen,

5
Chiamami anale, ma questa è concatenazione, non fusione. (E linq ha già un metodo Concat.)
OldFart

4

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

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 si applica per Dictionary<T>o altri tipi di raccolte.

Ecco la mia soluzione usando la yieldparola chiave:

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 fare un forgiro sulla mia collezione per trovare l'indice. Il mio pulsante ha un ID e questo viene anche usato come indice IndexerList<T>, quindi eviti ID o indici ridondanti : questo è quello che mi piace! L'indice / ID può essere un numero arbitrario.


2

Un esempio pratico può essere trovato qui:

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

I vantaggi dell'utilizzo del rendimento rispetto al codice standard sono numerosi:

  • Se l'iteratore viene utilizzato per creare un elenco, è possibile produrre il ritorno e il chiamante può decidere se desidera quel risultato in un elenco o meno.
  • Il chiamante può anche decidere di annullare l'iterazione per un motivo che non rientra nell'ambito di ciò che si sta facendo nell'iterazione.
  • Il codice è un po 'più breve.

Tuttavia, come ha detto Jan_V (basta battermi per alcuni secondi :-) puoi vivere senza di esso perché internamente il compilatore produrrà un codice quasi identico in entrambi i casi.


1

Ecco un esempio:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

La classe esegue i calcoli della data in base a una settimana lavorativa. Posso dire ad un'istanza della lezione che Bob lavora dalle 9:30 alle 17:30 ogni giorno della settimana con un'ora di pausa pranzo alle 12:30. Con questa conoscenza, la funzione AscendingShifts () produrrà oggetti turni di lavoro tra le date fornite. Per elencare tutti i turni di lavoro 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 scorre realmente su una raccolta. Tuttavia, i passaggi tra due date possono essere considerati come una raccolta. L' yieldoperatore consente di scorrere su questa raccolta immaginata senza creare la raccolta stessa.


1

Ho un piccolo livello di dati db che ha una commandclasse in cui si imposta il testo del comando SQL, il tipo di comando e si restituisce un IEnumerable di 'parametri di comando'.

Fondamentalmente l'idea è di avere digitato i comandi CLR invece di riempire manualmente SqlCommandproprietà e parametri in ogni momento.

Quindi c'è una funzione che assomiglia 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 commandclasse ha le proprietà Agee Name.

Quindi è possibile rinnovare un commandoggetto riempito le sue proprietà e passarlo a dbun'interfaccia che esegue effettivamente la chiamata di comando.

Tutto sommato rende davvero facile lavorare con i comandi SQL e tenerli digitati.


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 MarkChecksummetodo, ad esempio, è simile al seguente. 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 aggregati come Sum () in un metodo di enumerazione poiché attivano un processo di enumerazione separato.


1

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

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

0

Espandendo la risposta di Jan_V, ho appena colpito un caso del mondo reale ad esso correlato:

Avevo bisogno di usare le versioni Kernel32 di FindFirstFile / FindNextFile. Ottieni un handle dalla prima chiamata e lo trasmetti a tutte le chiamate successive. Avvolgilo in un enumeratore e otterrai qualcosa che puoi usare direttamente con foreach.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.