Domanda

Nel Come posso esporre solo un frammento di IList<> domanda una delle risposte aveva il seguente snippet di codice:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item )
            yield return item;
    }
}

Che cosa fa la parola chiave yield in questo caso?L'ho visto menzionato in un paio di posti e in un'altra domanda, ma non ho ancora capito cosa fa realmente.Sono abituato a pensare al rendimento nel senso che un filo cede a un altro, ma qui non sembra rilevante.

È stato utile?

Soluzione

IL yield la parola chiave in realtà fa molto qui.

La funzione restituisce un oggetto che implementa il IEnumerable<object> interfaccia.Se viene avviata una funzione di chiamata foreachPassando su questo oggetto, la funzione viene richiamata nuovamente finché non "cede".Questo è lo zucchero sintattico introdotto C#2.0.Nelle versioni precedenti dovevi crearne uno tuo IEnumerable E IEnumerator oggetti per fare cose del genere.

Il modo più semplice per comprendere un codice come questo è digitare un esempio, impostare alcuni punti di interruzione e vedere cosa succede.Prova a seguire questo esempio:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Se esamini l'esempio, troverai la prima chiamata a Integers() ritorna 1.Ritorna la seconda chiamata 2 e la linea yield return 1 non viene eseguito nuovamente.

Ecco un esempio di vita reale:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

Altri suggerimenti

Iterazione.Crea una macchina a stati "sotto le coperte" che ricorda dove ti trovavi in ​​ogni ciclo aggiuntivo della funzione e riprende da lì.

Il rendimento ha due grandi usi,

  1. Aiuta a fornire un'iterazione personalizzata senza creare raccolte temporanee.

  2. Aiuta a fare l'iterazione con stato.enter image description here

Per spiegare i due punti sopra in modo più dimostrativo, ho creato un semplice video che puoi guardare Qui

Recentemente anche Raymond Chen ha pubblicato un'interessante serie di articoli sulla parola chiave yield.

Sebbene sia nominalmente utilizzato per implementare facilmente un modello iteratore, ma può essere generalizzato in una macchina a stati.Inutile citare Raymond, l'ultima parte si collega anche ad altri usi (ma l'esempio nel blog di Entin è particolarmente buono, mostrando come scrivere codice sicuro asincrono).

A prima vista, yield return è uno zucchero .NET per restituire un IEnumerable.

Senza resa, tutti gli elementi della collezione vengono creati contemporaneamente:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Stesso codice che utilizza yield, restituisce articolo per articolo:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

Il vantaggio dell'utilizzo di yield è che se la funzione che consuma i tuoi dati necessita semplicemente del primo elemento della raccolta, il resto degli elementi non verrà creato.

L'operatore yield consente la creazione di articoli come richiesto.Questo è un buon motivo per usarlo.

yield return viene utilizzato con gli enumeratori.Ad ogni chiamata dell'istruzione yield, il controllo viene restituito al chiamante ma garantisce che lo stato del chiamato venga mantenuto.Per questo motivo, quando il chiamante enumera l'elemento successivo, continua l'esecuzione nel metodo chiamato dall'istruzione immediatamente dopo l'elemento yield dichiarazione.

Cerchiamo di capirlo con un esempio.In questo esempio, in corrispondenza di ogni riga ho menzionato l'ordine in cui scorre l'esecuzione.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Inoltre, lo stato viene mantenuto per ogni enumerazione.Supponiamo che io abbia un'altra chiamata Fibs() metodo, lo stato verrà reimpostato.

Intuitivamente, la parola chiave restituisce un valore dalla funzione senza lasciarla, ad es.nel tuo esempio di codice restituisce il valore current item valore e quindi riprende il ciclo.Più formalmente, viene utilizzato dal compilatore per generare codice per un file iteratore.Gli iteratori sono funzioni che restituiscono IEnumerable oggetti.IL MSDN ha molti articoli su di loro.

Un'implementazione di elenco o matrice carica immediatamente tutti gli elementi mentre l'implementazione di yield fornisce una soluzione di esecuzione posticipata.

In pratica, è spesso desiderabile eseguire la quantità minima di lavoro necessaria per ridurre il consumo di risorse di un'applicazione.

Ad esempio, potremmo avere un'applicazione che elabora milioni di record da un database.È possibile ottenere i seguenti vantaggi quando utilizziamo IEnumerable in un modello basato su pull con esecuzione differita:

  • Scalabilità, affidabilità e prevedibilità probabilmente miglioreranno poiché il numero di record non influisce in modo significativo sui requisiti di risorse dell’applicazione.
  • Prestazioni e reattività probabilmente miglioreranno poiché l'elaborazione può iniziare immediatamente invece di attendere il caricamento dell'intera raccolta.
  • Recuperabilità e utilizzo probabilmente miglioreranno poiché l'applicazione può essere arrestata, avviata, interrotta o fallire.Verranno persi solo gli elementi in corso rispetto al precaricamento di tutti i dati in cui è stata effettivamente utilizzata solo una parte dei risultati.
  • Elaborazione continua è possibile in ambienti in cui vengono aggiunti flussi di carico di lavoro costanti.

Ecco un confronto tra la creazione prima di una raccolta, ad esempio un elenco, e l'utilizzo di yield.

Esempio di elenco

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Uscita della console
Elenco contattiNegozio:Creazione del contatto 1
Elenco contattiNegozio:Creazione del contatto 2
Elenco contattiNegozio:Creazione del contatto 3
Pronto per scorrere la raccolta.

Nota:L'intera raccolta è stata caricata in memoria senza nemmeno chiedere un singolo elemento dell'elenco

Esempio di rendimento

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Uscita della console
Pronto per scorrere la raccolta.

Nota:La raccolta non è stata affatto eseguita.Ciò è dovuto alla natura di "esecuzione differita" di IEnumerable.La costruzione di un oggetto avverrà solo quando sarà realmente necessaria.

Chiamiamo nuovamente la raccolta e invertiamo il comportamento quando recuperiamo il primo contatto nella raccolta.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Uscita della console
Pronto per scorrere la raccolta
ContattaYieldStore:Creazione del contatto 1
Ciao Bob

Carino!Solo il primo contatto è stato costruito quando il cliente ha "tirato fuori" l'oggetto dalla collezione.

Ecco un modo semplice per comprendere il concetto:L'idea di base è, se vuoi una collezione che puoi usare "foreach", ma raccogliere gli elementi nella raccolta è costoso per qualche motivo (come interrogarli da un database), E spesso non avrai bisogno dell'intera raccolta, quindi crei una funzione che costruisce la raccolta un elemento alla volta e lo restituisce al consumatore (che può quindi interrompere anticipatamente lo sforzo di raccolta).

Pensare in questo modo: Vai al banco della carne e vuoi comprare mezzo chilo di prosciutto affettato.Il macellaio prende un prosciutto da 10 libbre sul retro, lo mette sull'affettatrice, affetta tutto, poi ti riporta la pila di fette e ne misura una libbra.(VECCHIO modo).Con yield, il macellaio porta l'affettatrice al bancone e inizia ad affettare e "cedere" ogni fetta sulla bilancia fino a quando non misura 1 libbra, quindi la avvolge per te e il gioco è fatto. Il Vecchio Modo può essere migliore per il macellaio (gli consente di organizzare i suoi macchinari come preferisce), ma il Nuovo Modo è chiaramente più efficiente nella maggior parte dei casi per il consumatore.

IL yield la parola chiave ti consente di creare un file IEnumerable<T> nel modulo su an blocco iteratore.Questo blocco iteratore supporta esecuzione differita e se non hai familiarità con il concetto può apparire quasi magico.Tuttavia, alla fine è solo codice che viene eseguito senza strani trucchi.

Un blocco iteratore può essere descritto come zucchero sintattico in cui il compilatore genera una macchina a stati che tiene traccia di quanto è progredita l'enumerazione degli enumerabili.Per enumerare un enumerabile, spesso si utilizza a foreach ciclo continuo.Tuttavia, a foreach loop è anche zucchero sintattico.Quindi sei a due astrazioni rimosse dal codice reale, motivo per cui inizialmente potrebbe essere difficile capire come funziona tutto insieme.

Supponiamo di avere un blocco iteratore molto semplice:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

I blocchi iteratori reali spesso hanno condizioni e cicli ma quando controlli le condizioni e svolgi i cicli finiscono comunque come yield istruzioni interlacciate con altro codice.

Per enumerare il blocco iteratore a foreach viene utilizzato il ciclo:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Ecco l'output (nessuna sorpresa qui):

Begin
1
After 1
2
After 2
42
End

Come detto sopra foreach è lo zucchero sintattico:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Nel tentativo di districare questo problema ho creato un diagramma di sequenza con le astrazioni rimosse:

C# iterator block sequence diagram

Anche la macchina a stati generata dal compilatore implementa l'enumeratore ma per rendere più chiaro il diagramma li ho mostrati come istanze separate.(Quando la macchina a stati viene enumerata da un altro thread, in realtà ottieni istanze separate ma questo dettaglio non è importante qui.)

Ogni volta che chiami il blocco iteratore viene creata una nuova istanza della macchina a stati.Tuttavia, nessuno dei tuoi codici nel blocco iteratore verrà eseguito fino a quando enumerator.MoveNext() viene eseguito per la prima volta.Ecco come funziona l'esecuzione differita.Ecco un esempio (piuttosto sciocco):

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

A questo punto l'iteratore non è stato eseguito.IL Where la clausola crea una nuova clausola IEnumerable<T> che avvolge il IEnumerable<T> restituito da IteratorBlock ma questo enumerabile deve ancora essere enumerato.Questo accade quando esegui a foreach ciclo continuo:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Se enumeri l'enumerabile due volte, ogni volta viene creata una nuova istanza della macchina a stati e il blocco iteratore eseguirà lo stesso codice due volte.

Si noti che i metodi LINQ come ToList(), ToArray(), First(), Count() eccetera.utilizzerà a foreach loop per enumerare l'enumerabile.Ad esempio ToList() enumererà tutti gli elementi dell'enumerabile e li memorizzerà in un elenco.È ora possibile accedere all'elenco per ottenere tutti gli elementi dell'enumerabile senza che il blocco iteratore venga nuovamente eseguito.Esiste un compromesso tra l'utilizzo della CPU per produrre più volte gli elementi dell'enumerabile e la memoria per archiviare gli elementi dell'enumerazione per accedervi più volte quando si utilizzano metodi come ToList().

Se ho capito bene, ecco come lo esprimerei dal punto di vista della funzione che implementa IEnumerable con yield.

  • Eccone uno.
  • Chiama di nuovo se ne hai bisogno di un altro.
  • Mi ricorderò quello che ti ho già dato.
  • Saprò se posso dartene un altro solo quando chiamerai di nuovo.

La parola chiave yield C#, per dirla semplicemente, consente molte chiamate a un corpo di codice, denominato iteratore, che sa come tornare prima che sia terminato e, quando richiamato di nuovo, continua da dove era stato interrotto, ovveroaiuta un iteratore a diventare trasparente con stato per ogni elemento in una sequenza che l'iteratore restituisce nelle chiamate successive.

In JavaScript, lo stesso concetto si chiama Generatori.

È un modo molto semplice e facile per creare un enumerabile per il tuo oggetto.Il compilatore crea una classe che racchiude il metodo e che implementa, in questo caso, IEnumerable<object>.Senza la parola chiave yield, dovresti creare un oggetto che implementi IEnumerable<object>.

Sta producendo una sequenza enumerabile.Ciò che fa è effettivamente creare una sequenza IEnumerable locale e restituirla come risultato del metodo

Questo collegamento ha un semplice esempio

Esempi ancora più semplici sono qui

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Si noti che il rendimento del rendimento non verrà restituito dal metodo.Puoi anche mettere a WriteLine dopo il yield return

Quanto sopra produce un IEnumerable di 4 int 4,4,4,4

Qui con a WriteLine.Aggiungerà 4 all'elenco, stamperà abc, quindi aggiungerà 4 all'elenco, quindi completerà il metodo e quindi restituirà effettivamente il metodo (una volta completato il metodo, come accadrebbe con una procedura senza ritorno).Ma questo avrebbe un valore, un IEnumerable lista di ints, che ritorna al completamento.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Nota anche che quando usi yield, ciò che stai restituendo non è dello stesso tipo della funzione.È del tipo di un elemento all'interno di IEnumerable elenco.

Utilizzi yield con il tipo restituito del metodo as IEnumerable.Se il tipo restituito del metodo è int O List<int> e usi yield, quindi non verrà compilato.Puoi usare IEnumerable metodo return type senza yield ma sembra che forse non sia possibile utilizzare yield senza IEnumerable tipo restituito del metodo.

E per farlo eseguire bisogna chiamarlo in un modo speciale.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

Sta cercando di portare un po' di Ruby Goodness :)
Concetto: Questo è un esempio di codice Ruby che stampa ogni elemento dell'array

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

L'implementazione di ciascun metodo dell'Array cede controllo sul chiamante (il 'mette x') con ogni elemento dell'array presentato ordinatamente come x.Il chiamante può quindi fare tutto ciò che deve fare con x.

Tuttavia .Netto non arriva fino in fondo qui..C# sembra aver accoppiato yield con IEnumerable, costringendoti in un certo senso a scrivere un ciclo foreach nel chiamante, come visto nella risposta di Mendelt.Un po' meno elegante.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top