Do czego służy słowo kluczowe yield w C#?

W Jak mogę odsłonić tylko Fragment IList pytanie jedna z odpowiedzi miała następujący fragment kodu:

IEnumerable<object> FilteredList()
{
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item )
            yield return item;
    }
}

Co tam robi słowo kluczowe yield? Widziałem to w kilku miejscach, i jeszcze jedno pytanie, ale nie do końca zorientowałem się, co to właściwie robi. Jestem przyzwyczajony do myślenia o plonowaniu w sensie jednego wątku ulegającego drugiemu, ale to nie wydaje się tutaj istotne.

 674
Author: Community, 2008-09-02

16 answers

Słowo kluczowe yield robi tu całkiem sporo. Funkcja zwraca obiekt, który implementuje interfejs IEnumerable. Jeśli funkcja wywołująca rozpocznie się przed każdym uruchomieniem nad tym obiektem, funkcja jest wywoływana ponownie, dopóki nie"uzyska". Jest to cukier składniowy wprowadzony w C # 2.0. We wcześniejszych wersjach trzeba było tworzyć własne obiekty IEnumerable i IEnumerator, aby robić takie rzeczy.

Najprostszym sposobem zrozumienia kodu w ten sposób jest wpisanie przykładu, ustawienie punktów przerwania i zobacz co się stanie.

Spróbuj przejść przez to na przykład:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Gdy przejdziesz przez przykład, zobaczysz, że pierwsze wywołanie funkcji Integers () zwraca 1. Drugie wywołanie zwraca 2 i linia "yield return 1" nie jest wykonywana ponownie.

Oto przykład prawdziwego życia

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}
 596
Author: Mendelt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-22 10:16:20

Iteracja. Tworzy maszynę stanową "pod pokrywami", która pamięta, gdzie byłeś w każdym dodatkowym cyklu funkcji i stamtąd odbiera.

 327
Author: Joel Coehoorn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-14 17:55:38

Wydajność ma dwa wielkie zastosowania,

  1. Pomaga zapewnić niestandardową iterację bez tworzenia tymczasowych kolekcji.

  2. To pomaga zrobić stateful iteracji. Tutaj wpisz opis obrazka

Aby wyjaśnić powyższe dwa punkty bardziej demonstracyjnie, stworzyłem prosty film, który możecie obejrzeć tutaj

 158
Author: Shivprasad Koirala,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-20 12:14:03

Ostatnio Raymond Chen prowadził również interesującą serię artykułów na temat słowa kluczowego yield.

Podczas gdy jest nominalnie używany do łatwej implementacji wzorca iteratora, ale można go uogólnić na maszynę stanową. Nie ma sensu cytować Raymonda, ostatnia część również linki do innych zastosowań (ale przykład na blogu Entina jest dobry esp, pokazujący jak pisać asynchroniczny kod).

 124
Author: Svend,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-27 08:31:18

Na pierwszy rzut oka, yield return to cukier. NET, który zwraca liczbę.

Bez plonu wszystkie elementy kolekcji są tworzone jednocześnie:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Ten sam kod używając yield, Zwraca pozycję po pozycji:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

Zaletą użycia yield jest to, że jeśli funkcja zużywająca dane po prostu potrzebuje pierwszego elementu kolekcji, reszta elementów nie zostanie utworzona.

Operator wydajności umożliwia tworzenie elementów zgodnie z wymaganiami. To dobrze. powód, by go użyć.

 50
Author: Marquinho Peli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-22 16:09:30

yield return jest używany z enumeratorami. Przy każdym wywołaniu instrukcji yield kontrola jest zwracana do wywołującego, ale zapewnia utrzymanie stanu wywołującego. Z tego powodu, gdy wywołujący wylicza następny element, kontynuuje wykonywanie w metodzie callee z polecenia bezpośrednio po instrukcji yield.

Spróbujmy to zrozumieć na przykładzie. W tym przykładzie, odpowiadającym każdej linii wspomniałem kolejność, w jakiej przebiega realizacja.
static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Również stan jest utrzymywany dla każdego wyliczenia. Załóżmy, że mam inne wywołanie metody Fibs(), wtedy stan zostanie dla niej zresetowany.

 29
Author: RKS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-18 07:53:17

Intuicyjnie, słowo kluczowe Zwraca wartość z funkcji bez opuszczania jej, tzn. w przykładzie kodu zwraca bieżącą wartość item, a następnie wznawia pętlę. Bardziej formalnie, jest on używany przez kompilator do generowania kodu dla iteratora . Iteratory są funkcjami zwracającymi IEnumerable obiekty. W MSDN znajduje się kilka artykułów na ich temat.

 26
Author: Konrad Rudolph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 13:19:09

Implementacja listy lub tablicy natychmiast ładuje wszystkie pozycje, podczas gdy implementacja yield zapewnia odroczone wykonanie.

W praktyce często pożądane jest wykonanie minimalnej ilości pracy w razie potrzeby w celu zmniejszenia zużycia zasobów aplikacji.

Na przykład, możemy mieć aplikację, która przetwarza miliony rekordów z bazy danych. Następujące korzyści można osiągnąć, gdy używamy IEnumerable w odroczonej egzekucji model oparty na pull:

  • skalowalność, niezawodność i przewidywalność prawdopodobnie ulegną poprawie, ponieważ liczba rekordów nie wpływa znacząco na wymagania aplikacji dotyczące zasobów.
  • wydajność i responsywność prawdopodobnie ulegną poprawie, ponieważ przetwarzanie może rozpocząć się natychmiast, zamiast czekać na załadowanie całej kolekcji jako pierwszej.
  • odzysk i wykorzystanie prawdopodobnie ulegną poprawie od czasu zastosowania można go zatrzymać, uruchomić, przerwać lub zawieść. Tylko elementy w toku zostaną utracone w porównaniu do wstępnego pobierania wszystkich danych, w przypadku gdy faktycznie użyto tylko części wyników.
  • przetwarzanie ciągłe jest możliwe w środowiskach, w których dodawane są strumienie stałych obciążeń.

Oto porównanie pomiędzy zbudowaniem kolekcji jako pierwszej, takiej jak lista, a użyciem yield.

Przykład Listy

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

konsola Wyjście
ContactListStore: Tworzenie kontaktu 1
ContactListStore: Tworzenie kontaktu 2
ContactListStore: Tworzenie kontaktu 3
Gotowy do iteracji poprzez kolekcję.

Uwaga: cała kolekcja została załadowana do pamięci, nawet nie pytając o pojedynczy element na liście

Przykład Wydajności

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Wyjście Konsoli
Gotowy do iteracji poprzez kolekcję.

Uwaga: kolekcja nie była w ogóle stracony. Wynika to z charakteru" odroczonej egzekucji " IEnumerable. Konstruowanie elementu nastąpi tylko wtedy, gdy jest to naprawdę wymagane.

Nazwijmy zbiór ponownie i odwróćmy zachowanie, gdy pobierzemy pierwszy kontakt w zbiorze.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Wyjście Konsoli
Gotowy do iteracji poprzez kolekcję
Kontakt: Tworzenie kontaktu 1
Hello Bob

Nieźle! Dopiero pierwszy kontakt powstał, gdy klient "wyciągnął" przedmiot z kolekcji.
 18
Author: Strydom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-11 22:12:00

Oto prosty sposób na zrozumienie pojęcia: Podstawową ideą jest to, że jeśli chcesz mieć kolekcję, na której możesz użyć "foreach", ale zbieranie przedmiotów do kolekcji jest z jakiegoś powodu drogie (np. odpytywanie ich z bazy danych) i często nie będziesz potrzebował całej kolekcji, wtedy tworzysz funkcję, która buduje kolekcję po jednym przedmiocie na raz i oddaje ją konsumentowi (który może wcześniej zakończyć pracę nad kolekcją).

Pomyśl o tym w ten sposób: Idziesz do baru z mięsem i chcesz kupić kilogram pokrojonej szynki. Rzeźnik bierze 10-funtową szynkę na plecy, kładzie ją na krajalnicy, kroi całość, a potem przynosi stertę plastrów z powrotem do Ciebie i mierzy jej Funt. (OLD way). Z yield, rzeźnik przynosi krajalnicę do lady, i rozpoczyna krojenie i" oddawanie " każdy kawałek na wagę, aż mierzy 1-funt, a następnie owija go dla Ciebie i gotowe. stary sposób może być lepszy dla rzeźnika (pozwala organizuje swoje maszyny tak, jak lubi), ale nowy sposób jest wyraźnie bardziej wydajny w większości przypadków dla konsumenta.

 13
Author: kmote,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-01 18:16:25

Słowo kluczowe yield pozwala utworzyć IEnumerable<T> w formularzu na blok iteratora. Ten blok iteratora obsługuje deferred executing i jeśli nie jesteś zaznajomiony z tym pojęciem, może wydawać się niemal magiczny. Jednak na koniec dnia jest to po prostu kod, który wykonuje się bez żadnych dziwnych sztuczek.

Blok iteratora można opisać jako cukier składniowy, gdzie kompilator generuje maszynę stanową, która śledzi, jak daleko wyliczenia / align = "left" / Aby wyliczyć liczbę, często używa się pętli foreach. Jednak pętla foreach jest również cukrem składniowym. Więc jesteście dwoma abstrakcjami usuniętymi z prawdziwego kodu, dlatego początkowo może być trudno zrozumieć, jak to wszystko działa razem.

Załóżmy, że masz bardzo prosty blok iteratora:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

Prawdziwe bloki iteratora często mają warunki i pętle, ale po sprawdzeniu warunków i rozwinięciu pętli nadal kończą się jako yield instrukcje przeplatane z innym kodem.

Do wyliczenia bloku iteratora używana jest pętla foreach:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Oto wyjście (bez niespodzianek tutaj):

Begin
1
After 1
2
After 2
42
End

Jak wspomniano powyżej foreach jest cukrem składniowym:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Próbując to rozwikłać, stworzyłem schemat sekwencji z usuniętymi abstrakcjami:

C# iterator block sequence diagram

Maszyna stanowa generowana przez kompilator również implementuje enumerator, ale aby uczynić diagram bardziej przejrzystym mam pokazano je jako osobne instancje. (Gdy maszyna stanowa jest wyliczana z innego wątku, w rzeczywistości otrzymujemy osobne instancje, ale ten szczegół nie jest tutaj ważny.)

Za każdym razem, gdy wywołujesz blok iteratora, tworzy się nowa instancja maszyny stanowej. Jednak żaden z kodu w bloku iteratora nie jest wykonywany, dopóki enumerator.MoveNext() nie zostanie wykonany po raz pierwszy. Tak działa odroczona realizacja. Oto (raczej głupi) przykład: {]}

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

W tym momencie iterator ma nie został stracony. Klauzula Where tworzy nową IEnumerable<T>, która otacza IEnumerable<T> zwróconą przez IteratorBlock, ale to wyliczenie nie zostało jeszcze wyliczone. Dzieje się tak, gdy wykonujesz pętlę foreach:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Jeśli wyliczysz liczbę dwukrotnie, to za każdym razem tworzona jest nowa instancja maszyny stanowej, a Twój blok iteratora wykona ten sam kod dwukrotnie.

Zauważ, że metody LINQ jak ToList(), ToArray(), First(), Count() itd. użyje pętli foreach do wyliczenia liczby. Na instancja ToList() wyliczy wszystkie elementy enumerable i zapisze je w liście. Możesz teraz uzyskać dostęp do listy, aby uzyskać wszystkie elementy enumerable bez ponownego uruchamiania bloku iteratora. Istnieje kompromis pomiędzy używaniem procesora do wytwarzania elementów wyliczenia wiele razy i pamięci do przechowywania elementów wyliczenia, aby uzyskać do nich dostęp wiele razy przy użyciu metod takich jak ToList().

 10
Author: Martin Liversage,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-28 11:54:13

Słowo kluczowe C# yield, mówiąc prościej, pozwala na wiele wywołań kodu, określanego jako iterator, który wie, jak powrócić przed wykonaniem i gdy zostanie wywołane ponownie, kontynuuje tam, gdzie zostało przerwane - tzn. pomaga iteratorowi stać się transparentnym statefuls dla każdego elementu w sekwencji, którą iterator zwraca w kolejnych wywołaniach.

W JavaScript to samo pojęcie nazywa się generatorami.

 9
Author: BoiseBaked,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-01 15:58:12

Jest to bardzo prosty i łatwy sposób na stworzenie wyliczenia dla Twojego obiektu. Kompilator tworzy klasę, która zawija metodę i która implementuje, w tym przypadku, IEnumerable. Bez słowa kluczowego yield, musisz utworzyć obiekt, który implementuje IEnumerable .

 5
Author: Will,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 13:17:36

Produkuje sekwencję enumerable. To, co robi, to tworzenie lokalnej sekwencji liczbowej i zwracanie jej jako wyniku metody

 3
Author: aku,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 13:18:25

Ten link ma prosty przykład

Jeszcze prostsze przykłady są tutaj

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Zauważ, że yield return nie powróci z metody. Możesz nawet umieścić WriteLine Po yield return

Powyższe daje iloraz 4 intów 4,4,4,4

Tutaj z WriteLine. Doda 4 do listy, wyświetli abc, następnie doda 4 do listy, następnie zakończy metodę i tak naprawdę powróci z metody(po zakończeniu metody, jak to się stało z procedurą bez zwrotu). Ale będzie to miało wartość, IEnumerable listę ints, która zostanie zwrócona po zakończeniu.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Zauważ również, że gdy używasz yield, to co zwracasz nie jest tego samego typu co funkcja. Jest typu elementu z listy IEnumerable.

Używasz yield z typem zwrotu metody jako IEnumerable. Jeśli typem zwracanym przez metodę jest int lub List<int> i użyjesz yield, to metoda nie zostanie skompilowana. Można użyć metody IEnumerable typu return bez uzysku, ale wydaje się, że nie można użyć yield bez IEnumerable metody return type.

I aby go wykonać, musisz wywołać go w specjalny sposób.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}
 1
Author: barlop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-18 07:51:35

Jeśli dobrze to Rozumiem, oto jak bym to ujął z perspektywy funkcji implementującej iloczyn z yield.

    Oto jeden. Zadzwoń jeszcze raz, jeśli będziesz potrzebował kolejnego. Będę pamiętał, co już ci dałem. Będę wiedział tylko, jeśli dam ci kolejną, gdy zadzwonisz ponownie.
 1
Author: Casey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-15 22:28:47

Próbuje przynieść jakąś rubinową dobroć :)
koncepcja: jest to przykładowy kod Ruby, który wypisuje każdy element tablicy

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

Każda implementacja metody tablicydaje kontrolę nad wywołującym ('puts x') zkażdym elementem tablicy starannie przedstawionym jako x. wywołujący może wtedy zrobić wszystko, co musi zrobić z x.

Jednak . Net nie idzie tutaj aż tak daleko.. C # wydaje się mieć sprzężony yield z IEnumerable, w sposób wymuszający zapis pętli foreach w wywołującym, jak widać w odpowiedzi Mendelta. Trochę mniej eleganckie.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}
 -2
Author: Gishu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 14:06:52