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 truewaar 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__0in 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
prevencurrentworden velden in onze klasse (<current>5__2en<prev>5__1in ildasm). In onze methode hebben we twee posities (<>1__state): eerst bij de openende accolade, tweede bijyield return. - Voert code uit tot volgende
yield returnofyield break/}. - Voor
yield returnresulterende waarde opgeslagen, zodat deCurrenteigenschap deze kan retourneren.trueis teruggekeerd. Op dit punt wordt de huidige status opnieuw opgeslagen voor de volgendeMoveNextaanroep. - Voor de
yield break/}-methode geeft deze gewoonfalsewat 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;
}