Szukaj…


Wprowadzenie

Kiedy używasz słowa kluczowego fed w instrukcji, wskazujesz, że metoda, operator lub get accessor, w którym się pojawia, jest iteratorem. Użycie wydajności do zdefiniowania iteratora eliminuje potrzebę jawnej dodatkowej klasy (klasy przechowującej stan dla wyliczenia) podczas implementowania wzorca IEnumerable i IEnumerator dla niestandardowego typu kolekcji.

Składnia

  • stopa zwrotu [TYPE]
  • spadek wydajności

Uwagi

Umieszczenie słowa kluczowego yield w metodzie z typem zwracanym IEnumerable , IEnumerable<T> , IEnumerator lub IEnumerator<T> nakazuje kompilatorowi wygenerowanie implementacji typu zwracanego ( IEnumerable lub IEnumerator ), który po zapętleniu uruchamia metoda do każdej „wydajności”, aby uzyskać każdy wynik.

yield kluczowe yield jest przydatne, gdy chcesz zwrócić „następny” element teoretycznie nieograniczonej sekwencji, więc obliczenie całej sekwencji przedtem byłoby niemożliwe, lub gdy obliczenie pełnej sekwencji wartości przed zwróceniem doprowadziłoby do niepożądanej przerwy dla użytkownika .

yield break można również zastosować do zakończenia sekwencji w dowolnym momencie.

Ponieważ yield kluczowe wymaga typ interfejsu iterator jako zwracanego typu, takich jak IEnumerable<T> , nie można korzystać z tego w sposób asynchroniczny jako Zwraca Task<IEnumerable<T>> obiektów.

Dalsza lektura

Prosta obsługa

yield kluczowe yield służy do definiowania funkcji, która zwraca IEnumerable lub IEnumerator (a także ich pochodne warianty rodzajowe), których wartości są generowane leniwie, gdy obiekt wywołujący iteruje po zwróconej kolekcji. Przeczytaj więcej o celu w sekcji uwag .

Poniższy przykład zawiera instrukcję return return, która znajduje się for pętli for .

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

Następnie możesz to nazwać:

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

Wyjście konsoli

4
5
6
...
14

Wersja demonstracyjna na żywo .NET Fiddle

Każda iteracja treści instrukcji foreach powoduje wywołanie funkcji iteratora Count . Każde wywołanie funkcji iteratora przechodzi do następnego wykonania instrukcji yield return , która ma miejsce podczas następnej iteracji pętli for .

Bardziej odpowiednie zastosowanie

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

Istnieją oczywiście inne sposoby uzyskania IEnumerable<User> z bazy danych SQL - to po prostu pokazuje, że możesz użyć yield aby zamienić wszystko, co ma semantykę „sekwencji elementów” w IEnumerable<T> którą ktoś może iterować .

Wczesne zakończenie

Można rozszerzyć funkcjonalność istniejących metod yield , przekazując jedną lub więcej wartości lub elementów, które mogłyby zdefiniować warunek końcowy w ramach funkcji, wywołując yield break aby zatrzymać wykonywanie wewnętrznej pętli.

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

Powyższa metoda będzie iterować od określonej pozycji start dopóki nie earlyTerminationSet napotkana jedna z wartości w parametrze earlyTerminationSet .

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

Wynik:

1
2)
3)
4
5
6

Wersja demonstracyjna na żywo .NET Fiddle

Poprawne sprawdzanie argumentów

Metoda iteratora nie jest wykonywana, dopóki nie zostanie wyliczona wartość zwracana. Dlatego korzystne jest zapewnienie warunków wstępnych poza iteratorem.

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

Kod strony wywołującej (użycie):

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

Wynik:

1
2)
3)
4
5
6
7
8
9
10

Wersja demonstracyjna na żywo .NET Fiddle

Gdy metoda wykorzystuje yield do wygenerowania wyliczalnego, kompilator tworzy maszynę stanów, która po iteracji uruchomi kod do yield . Następnie zwraca uzyskany przedmiot i zapisuje jego stan.

Oznacza to, że nie dowiesz się o niepoprawnych argumentach (przekazanie null itp.), Gdy wywołujesz metodę po raz pierwszy (ponieważ tworzy to maszynę stanu), tylko wtedy, gdy próbujesz uzyskać dostęp do pierwszego elementu (ponieważ dopiero wtedy kod znajduje się w metoda uruchamiana przez maszynę stanu). Zawijając go w normalną metodę, która najpierw sprawdza argumenty, możesz je sprawdzić po wywołaniu metody. To jest przykład szybkiego zawierania.

Gdy używasz C # 7+, funkcję CountCore można wygodnie ukryć w funkcji Count jako funkcję lokalną . Zobacz przykład tutaj .

Zwraca inny Enumerable w ramach metody zwracającej 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 };
}

Leniwa ocena

Dopiero gdy instrukcja foreach przesunie się do następnego elementu, blok iteratora ocenia do następnej instrukcji yield .

Rozważ następujący przykład:

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

Spowoduje to wygenerowanie:

Rozpoczęcie iteracji
Wewnątrz iteratora: 0
Wewnątrz foreach: 0
Wewnątrz iteratora: 1
Wewnątrz foreach: 1
Wewnątrz iteratora: 2
Wewnątrz foreach: 2

Zobacz demo

W konsekwencji:

  • „Rozpoczęcie iteracji” jest drukowane jako pierwsze, mimo że metoda iteratora została wywołana przed wierszem wypisującym ją, ponieważ linia Integers().Take(3); tak naprawdę nie rozpoczyna iteracji (nie IEnumerator.MoveNext() )
  • Linie drukowane w konsoli zmieniają się naprzemiennie między tą w metodzie iteratora i tą w foreach , a nie wszystkie w metodzie iteratora, oceniając najpierw
  • Program kończy się z powodu metody .Take() , mimo że metoda iteratora ma wartość while true której nigdy się nie wybija.

Spróbuj ... w końcu

Jeśli metoda iterator ma wydajność wewnątrz try...finally , a następnie powrócił IEnumerator wykona finally oświadczenie gdy Dispose nazywa na to, jak długo obecny punkt oceny jest wewnątrz try bloku.

Biorąc pod uwagę funkcję:

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

Dzwoniąc:

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

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

Następnie drukuje:

1

Zobacz demo

Dzwoniąc:

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

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

Następnie drukuje:

1
2)
W końcu wykonany

Zobacz demo

Wykorzystanie wydajności do utworzenia IEnumeratora podczas wdrażania IEnumerable

Interfejs IEnumerable<T> ma jedną metodę GetEnumerator() , która zwraca IEnumerator<T> .

Chociaż słowa kluczowego yield można użyć do bezpośredniego utworzenia IEnumerable<T> , można go również użyć dokładnie w ten sam sposób, aby utworzyć IEnumerator<T> . Jedyne, co się zmienia, to typ zwracanej metody.

Może to być przydatne, jeśli chcemy stworzyć własną klasę, która implementuje 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();
    }
}

(Należy zauważyć, że ten konkretny przykład jest tylko ilustracyjny i może być bardziej przejrzysty zaimplementowany za pomocą jednej metody iteratora zwracającej IEnumerable<T> .)

Chętna ocena

yield kluczowe yield pozwala na leniwą ocenę kolekcji. Wymuszone ładowanie całej kolekcji do pamięci nazywa się chętną oceną .

Poniższy kod pokazuje to:

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();

Wywołanie ToList , ToDictionary lub ToArray wymusi natychmiastową ocenę wyliczenia, pobierając wszystkie elementy do kolekcji.

Leniwy przykład oceny: liczby Fibonacciego

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

Jak to działa pod maską (zalecam dekompilację wynikowego pliku .exe w narzędziu IL Disaambler):

  1. Kompilator w języku C # generuje klasę implementującą IEnumerable<BigInteger> i IEnumerator<BigInteger> ( <Fibonacci>d__0 w ildasm).
  2. Ta klasa implementuje maszynę stanu. Stan składa się z aktualnej pozycji w metodzie i wartości zmiennych lokalnych.
  3. Najciekawsze kody znajdują się w bool IEnumerator.MoveNext() . Zasadniczo, co MoveNext() :
    • Przywraca bieżący stan. Zmienne takie jak prev i current stają się polami w naszej klasie ( <current>5__2 i <prev>5__1 w ildasm). W naszej metodzie mamy dwie pozycje ( <>1__state ): pierwsza na otwierającym <>1__state klamrowym, druga na <>1__state yield return .
    • Wykonuje kod do następnego yield return yield break lub yield break yield return / } .
    • W celu yield return wynikowa wartość jest zapisywana, więc Current właściwość może ją zwrócić. true jest zwracana. W tym momencie bieżący stan jest ponownie zapisywany do następnego wywołania MoveNext .
    • Dla metody yield break / } po prostu zwraca false co oznacza, że iteracja jest zakończona.

Zauważ również, że 10001-ta liczba ma 468 bajtów. Automat stanów zapisuje tylko zmienne current i prev jako pola. Chociaż jeśli chcielibyśmy zapisać wszystkie liczby w sekwencji od pierwszej do 10000, rozmiar zajętej pamięci wyniesie ponad 4 megabajty. Tak leniwa ocena, jeśli jest właściwie stosowana, może w niektórych przypadkach zmniejszyć ślad pamięci.

Różnica między granicą podziału a granicą plastyczności

Używanie yield break w przeciwieństwie do break może nie być tak oczywiste, jak mogłoby się wydawać. Istnieje wiele złych przykładów w Internecie, w których użycie tych dwóch jest wymienne i tak naprawdę nie pokazuje różnicy.

Mylące jest to, że oba słowa kluczowe (lub frazy kluczowe) mają sens tylko w pętlach ( foreach , while ...) Więc kiedy wybrać jedno z nich?

Ważne jest, aby zdawać sobie sprawę, że po użyciu słowa kluczowego yield w metodzie skutecznie zamieniasz tę metodę w iterator . Jedynym celem takiej metody jest następnie iteracja zbioru skończonego lub nieskończonego i uzyskanie (wyprowadzenie) jego elementów. Po osiągnięciu celu nie ma powodu, aby kontynuować wykonywanie metody. Czasami dzieje się to naturalnie w ostatnim nawiasie zamykającym metody } . Ale czasami chcesz przedwcześnie zakończyć tę metodę. W normalnej (nie iterującej) metodzie użyłbyś słowa kluczowego return . Ale nie można użyć return w iteratorze, należy użyć yield break . Innymi słowy, yield break dla iteratora jest taki sam jak return dla metody standardowej. Natomiast instrukcja break po prostu kończy najbliższą pętlę.

Zobaczmy kilka przykładów:

    /// <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
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow