C# Language
Utbyte nyckelord
Sök…
Introduktion
När du använder avkastningsnyckelordet i ett uttalande anger du att metoden, operatören eller får accessor där det visas är en iterator. Att använda avkastning för att definiera en iterator tar bort behovet av en uttrycklig extra klass (klassen som håller tillståndet för en uppräkning) när du implementerar IEnumerable- och IEnumerator-mönstret för en anpassad samlingstyp.
Syntax
- avkastningsavkastning [TYPE]
- avkastningsavbrott
Anmärkningar
Att sätta yield
i en metod med IEnumerable
, IEnumerable<T>
, IEnumerator
eller IEnumerator<T>
berättar kompilatorn att generera en implementering av IEnumerable
( IEnumerable
eller IEnumerator
) som, när den är loopad över, kör metod upp till varje "avkastning" för att få varje resultat.
yield
är användbart när du vill returnera "nästa" element i en teoretiskt obegränsad sekvens, så att beräkna hela sekvensen i förväg skulle vara omöjligt, eller när du beräknar hela sekvensen av värden innan återvändande skulle leda till en oönskad paus för användaren .
yield break
kan också användas för att avsluta sekvensen när som helst.
Eftersom yield
kräver en iteratorgränssnittstyp som IEnumerable<T>
, till exempel IEnumerable<T>
, kan du inte använda detta i en async-metod eftersom det returnerar ett Task<IEnumerable<T>>
-objekt.
Vidare läsning
Enkel användning
yield
används för att definiera en funktion som returnerar en IEnumerable
eller IEnumerator
(såväl som deras härledda generiska varianter) vars värden genereras latent när en uppringare itererar över den returnerade samlingen. Läs mer om syftet i kommentarerna .
Följande exempel har ett avkastningsuttalande som finns i en for
loop.
public static IEnumerable<int> Count(int start, int count)
{
for (int i = 0; i <= count; i++)
{
yield return start + i;
}
}
Då kan du kalla det:
foreach (int value in Count(start: 4, count: 10))
{
Console.WriteLine(value);
}
Konsolutgång
4
5
6
...
14
Live-demonstration på .NET Fiddle
Varje iteration av foreach
uttalandeorganet skapar ett samtal till Count
iterator-funktionen. Varje samtal till iteratorfunktionen fortsätter till nästa exekvering av yield return
, vilket sker under nästa iteration av for
loopen.
Mer relevant användning
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);
}
}
}
Det finns IEnumerable<User>
andra sätt att få en IEnumerable<User>
från en SQL-databas - detta visar bara att du kan använda yield
att förvandla allt som har "sekvens av element" semantik till en IEnumerable<T>
som någon kan iterera över .
Tidig uppsägning
Du kan utöka funktionaliteten för befintliga yield
genom att mata in ett eller flera värden eller element som kan definiera ett avslutande villkor inom funktionen genom att kalla ett yield break
att stoppa den inre slingan från att köras.
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++;
}
}
Ovannämnda metod skulle iterera från en given start
position tills ett av värdena inom earlyTerminationSet
påträffades.
// 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);
}
Produktion:
1
2
3
4
5
6
Live-demonstration på .NET Fiddle
Korrekt kontrollera argument
En iteratormetod exekveras inte förrän returvärdet räknas upp. Det är därför fördelaktigt att hävda förutsättningar utanför iteratorn.
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;
}
}
Sidkod för samtal (användning):
// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
Console.WriteLine(x);
}
Produktion:
1
2
3
4
5
6
7
8
9
10
Live-demonstration på .NET Fiddle
När en metod använder yield
att generera en summan skapar kompilatorn en tillståndsmaskin som när den itereras över kommer att köra kod upp till en yield
. Den returnerar sedan den avkastade artikeln och sparar dess tillstånd.
Det betyder att du inte kommer att ta reda på ogiltiga argument (vidarebefordra null
etc.) när du först anropar metoden (eftersom det skapar tillståndsmaskinen), bara när du försöker få åtkomst till det första elementet (eftersom det bara är koden i koden metoden körs av staten maskin). Genom att lägga in den i en normal metod som först kontrollerar argument kan du kontrollera dem när metoden heter. Detta är ett exempel på att misslyckas snabbt.
När du använder C # 7+ kan CountCore
funktionen CountCore
döljas i Count
funktionen som en lokal funktion . Se exempel här .
Returnera en annan Enumerable inom en metod som returnerar Enumerable
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 };
}
Lat utvärdering
Först när foreach
uttalandet flyttar till nästa objekt utvärderar iteratorblocket upp till nästa yield
.
Tänk på följande exempel:
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);
}
}
Detta kommer att matas ut:
Starta iteration
Inuti iterator: 0
Inuti förhand: 0
Inuti iterator: 1
Inuti förhand: 1
Inuti iterator: 2
Inuti förhand: 2
Som en konsekvens:
- "Starta iteration" skrivs först ut trots att iteratormetoden kallades innan linjen skriver ut den eftersom linjen
Integers().Take(3);
startar faktiskt inte iteration (inget samtal tillIEnumerator.MoveNext()
gjordes) - Linjerna som skriver ut för att trösta växlar mellan den inuti iteratormetoden och den som finns inuti
foreach
, snarare än alla som finns inuti iteratormetoden som först utvärderar - Detta program avslutas på grund av
.Take()
, även om iteratormetoden har ettwhile true
som det aldrig bryter ut från.
Försök ... äntligen
Om en iterator metod har en avkastning i en try...finally
, sedan den returnerade IEnumerator
kommer att utföra den finally
uttalande när Dispose
anropas på det, så länge som den aktuella punkten med utvärderingen är inne i try
block.
Med tanke på funktionen:
private IEnumerable<int> Numbers()
{
yield return 1;
try
{
yield return 2;
yield return 3;
}
finally
{
Console.WriteLine("Finally executed");
}
}
När du ringer:
private void DisposeOutsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Sedan skrivs det ut:
1
När du ringer:
private void DisposeInsideTry()
{
var enumerator = Numbers().GetEnumerator();
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.MoveNext();
Console.WriteLine(enumerator.Current);
enumerator.Dispose();
}
Sedan skrivs det ut:
1
2
Slutligen avrättad
Använd avkastning för att skapa en IEnumerator vid implementering av IEnumerable
IEnumerable<T>
-gränssnittet har en enda metod, GetEnumerator()
, som returnerar en IEnumerator<T>
.
Medan yield
kan användas för att direkt skapa en IEnumerable<T>
, kan den också användas på exakt samma sätt för att skapa en IEnumerator<T>
. Det enda som ändras är metodens returtyp.
Detta kan vara användbart om vi vill skapa vår egen klass som implementerar IEnumerable<T>
:
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();
}
}
(Observera att det här exemplet bara är illustrativt och kan implementeras mer rent med en enda iteratormetod som returnerar en IEnumerable<T>
.)
Ivrig utvärdering
yield
tillåter lat utvärdering av samlingen. Att tvinga hela samlingen i minnet kallas ivrig utvärdering .
Följande kod visar detta:
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();
Calling ToList
, ToDictionary
eller ToArray
kommer att tvinga omedelbar utvärdering av uppräkningen och hämta alla elementen i en samling.
Lazy Evaluation Exempel: Fibonacci-nummer
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));
}
}
}
Hur det fungerar under huven (jag rekommenderar att dekompilera resulterande .exe-fil i IL Disaambler-verktyget):
- C # -kompileraren genererar en klass som implementerar
IEnumerable<BigInteger>
ochIEnumerator<BigInteger>
(<Fibonacci>d__0
i ildasm). - Denna klass implementerar en tillståndsmaskin. Tillstånd består av aktuell position i metod och värden för lokala variabler.
- Den mest intressanta koden finns i
bool IEnumerator.MoveNext()
-metoden. I princip vadMoveNext()
gör:- Återställer det aktuella tillståndet. Variabler som
prev
ochcurrent
blir fält i vår klass (<current>5__2
och<prev>5__1
i ildasm). I vår metod har vi två positioner (<>1__state
): först vid öppningens lockiga stag, därefter vidyield return
. - Kör kod tills nästa
yield return
elleryield break
/}
. - För
yield return
sparas det resulterande värdet så attCurrent
egendom kan returnera det.true
returneras. Vid denna punkt sparas det aktuella tillståndet igen för nästaMoveNext
. - För
yield break
/}
-metoden returnerar barafalse
innebär att iteration görs.
- Återställer det aktuella tillståndet. Variabler som
Observera också att 10001: e talet är 468 byte långt. Tillståndsmaskin sparar bara current
och prev
variabler som fält. Om vi vill spara alla siffror i sekvensen från den första till den 10000: e, kommer den förbrukade minnesstorleken att vara över 4 megabyte. Så lat utvärdering, om den används korrekt, kan minska fotavtrycket i vissa fall.
Skillnaden mellan paus och avkastning
Att använda yield break
motsats till break
kanske inte är så uppenbart som man tror. Det finns många dåliga exempel på Internet där användningen av de två är utbytbara och inte riktigt visar skillnaden.
Den förvirrande delen är att båda nyckelorden (eller nyckelfraser) är meningsfulla endast inom öglor ( foreach
, while
...) Så när ska man välja det ena till det andra?
Det är viktigt att inse att när du väl använder yield
i en metod förvandlar du metoden till en iterator . Det enda syftet med en sådan metod är då att iterera över en begränsad eller oändlig samling och ge (utgöra) dess element. När syftet har uppfyllts finns det ingen anledning att fortsätta metodens körning. Ibland händer det naturligt med metodens sista stängningsfäste }
. Men ibland vill du avsluta metoden för tidigt. I en normal metod (icke-iteration) skulle du använda return
sökord. Men du kan inte använda return
i en iterator, du måste använda yield break
. Med andra ord är yield break
för en iterator detsamma som return
för en standardmetod. Medan den break
uttalande just avslutar närmast slingan.
Låt oss se några exempel:
/// <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;
}