Zoeken…


Invoering

Wanneer u het opbrengstwoord in een instructie gebruikt, geeft u aan dat de methode, operator of get accessor waarin deze wordt weergegeven een iterator is. Wanneer u yield gebruikt om een iterator te definiëren, is er geen expliciete extra klasse nodig (de klasse die de status voor een opsomming heeft) wanneer u het patroon IEnumerable en IEnumerator implementeert voor een aangepast verzamelingstype.

Syntaxis

  • opbrengst rendement [TYPE]
  • opbrengst breken

Opmerkingen

Door het yield sleutelwoord in een methode te plaatsen met het IEnumerable , IEnumerable<T> , IEnumerator of IEnumerator<T> vertelt de compiler een implementatie van het IEnumerable ( IEnumerable of IEnumerator ) dat, wanneer het wordt doorgelust, de methode tot elke "opbrengst" om elk resultaat te krijgen.

Het yield is handig wanneer u "het volgende" element van een theoretisch onbeperkte reeks wilt retourneren, dus het vooraf berekenen van de volledige reeks onmogelijk zou zijn, of wanneer het berekenen van de volledige reeks waarden vóór terugkeer zou leiden tot een ongewenste pauze voor de gebruiker .

yield break kan ook worden gebruikt om de reeks op elk gewenst moment te beëindigen.

Omdat het yield sleutelwoord een iterator-interfacetype vereist als het retourtype, zoals IEnumerable<T> , kunt u dit niet gebruiken in een async-methode omdat dit een Task<IEnumerable<T>> retourneert.

Verder lezen

Eenvoudig gebruik

Het yield sleutelwoord wordt gebruikt om een functie te definiëren die een IEnumerable of IEnumerator retourneert (evenals hun afgeleide generieke varianten) waarvan de waarden lui worden gegenereerd als een beller de geretourneerde verzameling herhaalt. Lees meer over het doel in het opmerkingengedeelte .

Het volgende voorbeeld heeft een rendement-rendement-instructie die zich in een for lus bevindt.

public static IEnumerable<int> Count(int start, int count)
{
    for (int i = 0; i <= count; i++)
    {
        yield return start + i;
    }
}

Dan kun je het noemen:

foreach (int value in Count(start: 4, count: 10))
{
    Console.WriteLine(value);
}

Console-uitgang

4
5
6
...
14

Live demo op .NET Fiddle

Elke iteratie van het foreach statement body roept een aanroep op naar de functie Count iterator. Elke aanroep van de iteratorfunctie gaat door naar de volgende uitvoering van de yield return , die plaatsvindt tijdens de volgende iteratie van de for lus.

Meer relevant gebruik

public IEnumerable<User> SelectUsers()
{
    // Execute an SQL query on a database.
    using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
    {
        while (reader.Read())
        {
            int id = reader.GetInt32(0);
            string name = reader.GetString(1);
            yield return new User(id, name);
        }
    }
}

Er zijn natuurlijk ook andere manieren om een IEnumerable<User> uit een SQL-database te halen - dit toont alleen maar aan dat je yield kunt gebruiken om alles met een "opeenvolging van elementen" semantiek om te zetten in een IEnumerable<T> waarover iemand kan doorgaan .

Vroegtijdige beëindiging

U kunt de functionaliteit van bestaande yield uitbreiden door een of meer waarden of elementen door te geven die een afsluitende voorwaarde binnen de functie kunnen definiëren door een yield break aan te roepen yield break te voorkomen dat de binnenste lus wordt uitgevoerd.

public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
    int curr = start;

    while (true)
    {
        if (earlyTerminationSet.Contains(curr))
        {
            // we've hit one of the ending values
            yield break;
        }

        yield return curr;

        if (curr == Int32.MaxValue)
        {
            // don't overflow if we get all the way to the end; just stop
            yield break;
        }

        curr++;
    }
}

De bovenstaande werkwijze zou herhalen van een bepaalde start positie tot een van de waarden in de earlyTerminationSet aangetroffen.

// Iterate from a starting point until you encounter any elements defined as 
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
    // This will write out the results from 1 until 7 (which will trigger terminating)
    Console.WriteLine(x);
}

Output:

1
2
3
4
5
6

Live demo op .NET Fiddle

Argumenten correct controleren

Een iteratiemethode wordt pas uitgevoerd als de retourwaarde is opgesomd. Het is daarom voordelig om randvoorwaarden buiten de iterator te laten gelden.

public static IEnumerable<int> Count(int start, int count)
{
    // The exception will throw when the method is called, not when the result is iterated
    if (count < 0)
        throw new ArgumentOutOfRangeException(nameof(count));

    return CountCore(start, count);
}

private static IEnumerable<int> CountCore(int start, int count)
{
    // If the exception was thrown here it would be raised during the first MoveNext()
    // call on the IEnumerator, potentially at a point in the code far away from where
    // an incorrect value was passed.
    for (int i = 0; i < count; i++)
    {
        yield return start + i;
    }
}

Bellen zijcode (gebruik):

// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
    Console.WriteLine(x);
}

Output:

1
2
3
4
5
6
7
8
9
10

Live demo op .NET Fiddle

Wanneer een methode yield gebruikt om een opsomming te genereren, maakt de compiler een toestandsmachine die bij herhaling code tot een yield zal uitvoeren. Het retourneert vervolgens het geleverde item en slaat de status op.

Dit betekent dat u niet te weten komt over ongeldige argumenten ( null doorgeven enz.) Wanneer u de methode voor het eerst aanroept (omdat dat de statusmachine maakt), alleen wanneer u probeert toegang te krijgen tot het eerste element (omdat alleen dan de code binnen de methode wordt uitgevoerd door de statusmachine). Door het in een normale methode te verpakken die eerst argumenten controleert, kunt u deze controleren wanneer de methode wordt aangeroepen. Dit is een voorbeeld van snel falen.

Bij gebruik van C # 7+ kan de CountCore functie handig worden verborgen in de Count functie als een lokale functie . Zie voorbeeld hier .

Retourneer een andere Enumerable binnen een methode die Enumerable retourneert

public IEnumerable<int> F1()
{
    for (int i = 0; i < 3; i++)
        yield return i;

    //return F2(); // Compile Error!!
    foreach (var element in F2())
        yield return element;
}

public int[] F2()
{
    return new[] { 3, 4, 5 };
}

Luie evaluatie

Alleen wanneer de foreach instructie naar het volgende item gaat, evalueert het iteratorblok tot de volgende yield instructie.

Overweeg het volgende voorbeeld:

private IEnumerable<int> Integers()
{
    var i = 0;
    while(true)
    {
        Console.WriteLine("Inside iterator: " + i);
        yield return i;
        i++;
    }
}

private void PrintNumbers()
{
    var numbers = Integers().Take(3);
    Console.WriteLine("Starting iteration");

    foreach(var number in numbers)
    {
        Console.WriteLine("Inside foreach: " + number);
    }
}

Dit levert het volgende op:

Start iteratie
Binnen iterator: 0
Binnenkant foreach: 0
Binnen iterator: 1
Binnenkant foreach: 1
Binnen iterator: 2
Binnenkant: 2

Demo bekijken

Bijgevolg:

  • "Start iteratie" wordt eerst afgedrukt, ook al werd de iteratormethode aangeroepen voordat de regel werd afgedrukt omdat de regel Integers().Take(3); start iteratie niet (geen aanroep van IEnumerator.MoveNext() is gemaakt)
  • De regels die op de console worden afgedrukt, wisselen af tussen die in de iterator-methode en die in de foreach , in plaats van alle regels in de iterator-methode die als eerste worden geëvalueerd
  • Dit programma wordt beëindigd vanwege de .Take() -methode, hoewel de iterator-methode een while true waar het nooit uitkomt.

Probeer ... eindelijk

Als een iterator methode heeft een rendement in een try...finally , dan is de geretourneerde IEnumerator zal het uit te voeren finally statement wanneer Dispose wordt aangeroepen, zolang het huidige punt van de evaluatie is in de try -blok.

Gezien de functie:

private IEnumerable<int> Numbers()
{
    yield return 1;
    try
    {
        yield return 2;
        yield return 3;
    }
    finally
    {
        Console.WriteLine("Finally executed");
    }
}

Bij het bellen:

private void DisposeOutsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Daarna wordt afgedrukt:

1

Demo bekijken

Bij het bellen:

private void DisposeInsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

Daarna wordt afgedrukt:

1
2
Eindelijk uitgevoerd

Demo bekijken

Opbrengst gebruiken om een IEnumerator te maken bij het implementeren van IEnumerable

De interface IEnumerable<T> heeft één methode, GetEnumerator() , die een IEnumerator<T> retourneert.

Hoewel het yield kan worden gebruikt om rechtstreeks een IEnumerable<T> , kan het ook op precies dezelfde manier worden gebruikt om een IEnumerator<T> . Het enige dat verandert, is het retourtype van de methode.

Dit kan handig zijn als we onze eigen klasse willen maken die IEnumerable<T> implementeert:

public class PrintingEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _wrapped;

    public PrintingEnumerable(IEnumerable<T> wrapped)
    {
        _wrapped = wrapped;
    }

    // This method returns an IEnumerator<T>, rather than an IEnumerable<T>
    // But the yield syntax and usage is identical.
    public IEnumerator<T> GetEnumerator()
    {
        foreach(var item in _wrapped)
        {
            Console.WriteLine("Yielding: " + item);
            yield return item;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

(Merk op dat dit specifieke voorbeeld slechts ter illustratie is en beter zou kunnen worden geïmplementeerd met een enkele iteratiemethode die een IEnumerable<T> retourneert.)

Enorme evaluatie

Met het yield kan de collectie lui worden geëvalueerd. Het met geweld laden van de hele verzameling in het geheugen wordt enthousiaste evaluatie genoemd .

De volgende code laat dit zien:

IEnumerable<int> myMethod()
{
    for(int i=0; i <= 8675309; i++)
    {
        yield return i;
    }
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();

Als u ToList , ToDictionary of ToArray wordt de onmiddellijke evaluatie van de opsomming ToArray worden alle elementen in een verzameling opgehaald.

Lazy Evaluation Example: Fibonacci Numbers

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics

namespace ConsoleApplication33
{
    class Program
    {
        private static IEnumerable<BigInteger> Fibonacci()
        {
            BigInteger prev = 0;
            BigInteger current = 1;
            while (true)
            {
                yield return current;
                var next = prev + current;
                prev = current;
                current = next;
            }
        }

        static void Main()
        {
            // print Fibonacci numbers from 10001 to 10010
            var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
            Console.WriteLine(string.Join(Environment.NewLine, numbers));
        }
    }
}

Hoe het werkt onder de motorkap (ik raad aan het resulterende .exe-bestand te decompileren in het hulpprogramma IL Disaambler):

  1. C # compiler genereert een klasse die IEnumerable<BigInteger> en IEnumerator<BigInteger> ( <Fibonacci>d__0 in ildasm) implementeert.
  2. Deze klasse implementeert een statusmachine. Status bestaat uit huidige positie in methode en waarden van lokale variabelen.
  3. De meest interessante code zit in de bool IEnumerator.MoveNext() methode. Kort gezegd, wat MoveNext() doet:
    • Herstelt de huidige status. Variabelen zoals prev en current worden velden in onze klasse ( <current>5__2 en <prev>5__1 in ildasm). In onze methode hebben we twee posities ( <>1__state ): eerst bij de openende accolade, tweede bij yield return .
    • Voert code uit tot volgende yield return of yield break / } .
    • Voor yield return resulterende waarde opgeslagen, zodat de Current eigenschap deze kan retourneren. true is teruggekeerd. Op dit punt wordt de huidige status opnieuw opgeslagen voor de volgende MoveNext aanroep.
    • Voor de yield break / } -methode geeft deze gewoon false wat betekent dat iteratie is voltooid.

Merk ook op dat het 10001ste nummer 468 bytes lang is. Statusmachine slaat alleen current en prev variabelen op als velden. Terwijl als we alle nummers in de reeks van de eerste tot de 10.000e willen opslaan, de gebruikte geheugengrootte meer dan 4 megabytes zal zijn. Dus luie evaluatie, indien correct gebruikt, kan in sommige gevallen de geheugenafdruk verminderen.

Het verschil tussen break en yield break

Het gebruik van yield break in tegenstelling tot break misschien niet zo vanzelfsprekend als men zou denken. Er zijn veel slechte voorbeelden op internet waarbij het gebruik van de twee onderling uitwisselbaar is en het verschil niet echt aantoont.

Het verwarrende deel is dat beide sleutelwoorden (of sleutelzinnen) alleen zinvol zijn binnen lussen ( foreach , while ...) Dus wanneer kiezen voor de ene boven de andere?

Het is belangrijk om te beseffen dat als u het yield in een methode gebruikt, u de methode effectief in een iterator verandert . Het enige doel van deze methode is dan om een eindige of oneindige verzameling te herhalen en de elementen ervan op te leveren (output). Als het doel is bereikt, is er geen reden meer om door te gaan met de uitvoering van de methode. Soms gebeurt het natuurlijk met de laatste sluitstreep van de methode } . Maar soms wil je de methode voortijdig beëindigen. In een normale (niet-itererende) methode zou u het trefwoord return . Maar je kunt return niet gebruiken in een iterator, je moet een yield break . Met andere woorden, yield break voor een iterator is hetzelfde als return voor een standaardmethode. Terwijl de break instructie gewoon de dichtstbijzijnde lus beëindigt.

Laten we enkele voorbeelden bekijken:

    /// <summary>
    /// Yields numbers from 0 to 9
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
    public static IEnumerable<int> YieldBreak()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Indicates that the iteration has ended, everything 
                // from this line on will be ignored
                yield break;
            }
        }
        yield return 10; // This will never get executed
    }
    /// <summary>
    /// Yields numbers from 0 to 10
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
    public static IEnumerable<int> Break()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Terminates just the loop
                break;
            }
        }
        // Execution continues
        yield return 10;
    }


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow