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

Visa demo

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 till IEnumerator.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 ett while 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

Visa demo

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

Visa demo

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):

  1. C # -kompileraren genererar en klass som implementerar IEnumerable<BigInteger> och IEnumerator<BigInteger> ( <Fibonacci>d__0 i ildasm).
  2. Denna klass implementerar en tillståndsmaskin. Tillstånd består av aktuell position i metod och värden för lokala variabler.
  3. Den mest intressanta koden finns i bool IEnumerator.MoveNext() -metoden. I princip vad MoveNext() gör:
    • Återställer det aktuella tillståndet. Variabler som prev och current 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 vid yield return .
    • Kör kod tills nästa yield return eller yield break / } .
    • För yield return sparas det resulterande värdet så att Current egendom kan returnera det. true returneras. Vid denna punkt sparas det aktuella tillståndet igen för nästa MoveNext .
    • För yield break / } -metoden returnerar bara false innebär att iteration görs.

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;
    }


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow