C# Language
Opbrengst trefwoord
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
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
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
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
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 vanIEnumerator.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 eenwhile 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
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
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):
- C # compiler genereert een klasse die
IEnumerable<BigInteger>
enIEnumerator<BigInteger>
(<Fibonacci>d__0
in ildasm) implementeert. - Deze klasse implementeert een statusmachine. Status bestaat uit huidige positie in methode en waarden van lokale variabelen.
- De meest interessante code zit in de
bool IEnumerator.MoveNext()
methode. Kort gezegd, watMoveNext()
doet:- Herstelt de huidige status. Variabelen zoals
prev
encurrent
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 bijyield return
. - Voert code uit tot volgende
yield return
ofyield break
/}
. - Voor
yield return
resulterende waarde opgeslagen, zodat deCurrent
eigenschap deze kan retourneren.true
is teruggekeerd. Op dit punt wordt de huidige status opnieuw opgeslagen voor de volgendeMoveNext
aanroep. - Voor de
yield break
/}
-methode geeft deze gewoonfalse
wat betekent dat iteratie is voltooid.
- Herstelt de huidige status. Variabelen zoals
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;
}