Jaki jest cel / zaleta używania iteratorów zwrotu zysku w języku C #?


80

Wszystkie przykłady użycia yield return x;wewnątrz metody C #, które widziałem, można wykonać w ten sam sposób, zwracając po prostu całą listę. Czy w takich przypadkach jest jakaś korzyść lub korzyść z używania yield returnskładni w porównaniu z zwracaniem listy?

Ponadto w jakich scenariuszach można yield returnby użyć, że nie można po prostu zwrócić pełnej listy?


15
Dlaczego zakładasz, że na pierwszym miejscu jest „lista”? A jeśli nie ma?
Eric Lippert

2
@Eric, myślę, że właśnie o to pytałem. Kiedy nie miałbyś listy w pierwszej kolejności. Strumienie plików i nieskończone sekwencje to dwa świetne przykłady dotychczasowych odpowiedzi.
CoderDennis

1
Jeśli masz listę, po prostu ją zwróć; ale jeśli tworzysz listę wewnątrz metody i zwracasz ją, możesz / powinieneś zamiast tego używać iteratorów. Wydawaj przedmioty pojedynczo. Korzyści jest wiele.
justin.m.chase

2
Z pewnością wiele się nauczyłem, odkąd zadałem to pytanie 5 lat temu!
CoderDennis,

1
Największą zaletą posiadania yields jest to, że nie musisz nazywać kolejnej zmiennej pośredniej.
nawfal

Odpowiedzi:


120

Ale co by było, gdybyś sam tworzył kolekcję?

Ogólnie rzecz biorąc, iteratory mogą służyć do leniwego generowania sekwencji obiektów . Na przykład Enumerable.Rangemetoda nie ma wewnętrznie żadnego rodzaju kolekcji. Po prostu generuje następną liczbę na żądanie . Jest wiele zastosowań tego leniwego generowania sekwencji przy użyciu maszyny stanów. Większość z nich jest objęta koncepcjami programowania funkcjonalnego .

Moim zdaniem, jeśli traktujesz iteratory jako sposób wyliczania w kolekcji (to tylko jeden z najprostszych przypadków użycia), idziesz w złą stronę. Jak powiedziałem, iteratory są środkami do zwracania sekwencji. Sekwencja może być nawet nieskończona . Nie byłoby możliwości zwrócenia listy o nieskończonej długości i wykorzystania pierwszych 100 pozycji. To ma być leniwy czasami. Zwracanie kolekcji znacznie różni się od zwracania generatora kolekcji (którym jest iterator). Porównuje jabłka do pomarańczy.

Hipotetyczny przykład:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

Ten przykład wyświetla liczby pierwsze mniejsze niż 10000. Można to łatwo zmienić, aby wypisać liczby mniejsze niż milion, bez dotykania algorytmu generowania liczb pierwszych. W tym przykładzie nie możesz zwrócić listy wszystkich liczb pierwszych, ponieważ sekwencja jest nieskończona, a konsument nawet nie wie, ile elementów chce od początku.


Dobrze. Zbudowałem listę, ale czym różni się zwracanie jednej pozycji na raz w porównaniu z zwracaniem całej listy?
CoderDennis

4
Między innymi sprawia, że ​​kod jest bardziej modułowy, dzięki czemu można załadować element, przetworzyć, a następnie powtórzyć. Weź również pod uwagę przypadek, w którym załadowanie przedmiotu jest bardzo drogie lub jest ich dużo (mówią miliony). W takich przypadkach załadowanie całej listy jest niepożądane.
Dana the Sane

15
@ Dennis: W przypadku listy przechowywanej liniowo w pamięci może nie mieć różnicy, ale gdybyś na przykład wyliczył plik 10 GB i przetworzył każdą linię jeden po drugim, miałoby to znaczenie.
mmx

1
+1 za doskonałą odpowiedź - dodałbym również, że słowo kluczowe yield pozwala na zastosowanie semantyki iteratora do źródeł, które nie są tradycyjnie uważane za kolekcje - takich jak gniazda sieciowe, usługi internetowe, a nawet problemy z współbieżnością (patrz stackoverflow.com/questions/ 481714 / ccr-yield-and-vb-net )
LBushkin,

Ładny przykład, więc w zasadzie jest to generator kolekcji, który jest oparty na kontekście (np. Wywołanie metody) i nie uruchamia się, dopóki coś nie spróbuje uzyskać do niego dostępu, podczas gdy tradycyjna metoda zbierania bez uzysku musiałaby znać jego rozmiar, aby zbudować i zwróć pełną kolekcję - a następnie powtórzyć wymaganą część tej kolekcji?
Michael Harper,

24

Dobre odpowiedzi sugerują, że zaletą yield returnjest to , że nie trzeba tworzyć listy ; Listy mogą być drogie. (Po chwili uznasz je za nieporęczne i nieeleganckie).

Ale co, jeśli nie masz listy?

yield returnumożliwia przechodzenie po strukturach danych (niekoniecznie Listach) na wiele sposobów. Na przykład, jeśli twój obiekt jest drzewem, możesz przechodzić przez węzły w kolejności przed lub po, bez tworzenia innych list lub zmieniania podstawowej struktury danych.

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}

1
Ten przykład również podkreśla przypadek delegacji. Jeśli masz kolekcję, która w pewnych okolicznościach może zawierać elementy innych kolekcji, bardzo łatwo jest iterować i używać zwrotu zysku zamiast budowania pełnej listy wszystkich wyników i zwracania jej.
Tom Mayfield,

1
Teraz C # musi tylko zaimplementować yield!sposób, w jaki robi to F #, aby nie były potrzebne wszystkie foreachinstrukcje.
CoderDennis

Nawiasem mówiąc, twój przykład pokazuje jedno z „niebezpieczeństw” yield return: często nie jest oczywiste, kiedy wygeneruje wydajny lub nieefektywny kod. Chociaż yield returnmoże być używane rekurencyjnie, takie użycie spowoduje znaczny narzut na przetwarzanie głęboko zagnieżdżonych modułów wyliczających. Ręczne zarządzanie stanami może być bardziej skomplikowane w kodzie, ale działa znacznie wydajniej.
supercat

17

Leniwa ocena / odroczone wykonanie

Bloki iteratora „yield return” nie wykonają żadnego kodu, dopóki nie wywołasz tego konkretnego wyniku. Oznacza to, że można je również skutecznie łączyć łańcuchami. Quiz: ile razy następujący kod będzie iterował po pliku?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

Odpowiedź jest dokładnie jedna, i to dopiero na końcu foreachpętli. Mimo, że mam trzy oddzielne funkcje operatora linq, nadal przechodzimy przez zawartość pliku tylko raz.

Ma to inne zalety niż wydajność. Na przykład mogę napisać dość prostą i ogólną metodę jednorazowego odczytu i wstępnego przefiltrowania pliku dziennika i użyć tej samej metody w kilku różnych miejscach, gdzie każde użycie dodaje inne filtry. W ten sposób utrzymuję dobrą wydajność, jednocześnie efektywnie ponownie wykorzystując kod.

Nieskończone listy

Zobacz moją odpowiedź na to pytanie na dobry przykład:
funkcja C # Fibonacciego zwraca błędy

Zasadniczo implementuję sekwencję Fibonacciego za pomocą bloku iteratora, który nigdy się nie zatrzyma (przynajmniej nie przed osiągnięciem MaxInt), a następnie używam tej implementacji w bezpieczny sposób.

Ulepszona semantyka i separacja problemów

Ponownie, używając powyższego przykładu pliku, możemy teraz łatwo oddzielić kod, który odczytuje plik, od kodu, który odfiltrowuje niepotrzebne wiersze z kodu, który faktycznie analizuje wyniki. Szczególnie ten pierwszy jest bardzo przydatny do ponownego użycia.

Jest to jedna z tych rzeczy, które znacznie trudniej wyjaśnić prozą niż tylko komu za pomocą prostej grafiki 1 :

Separacja imperatywna a funkcjonalna obaw

Jeśli nie widzisz obrazu, przedstawia dwie wersje tego samego kodu z podświetleniami w tle dla różnych problemów. W kodzie linq wszystkie kolory są ładnie pogrupowane, podczas gdy w tradycyjnym kodzie rozkazującym kolory są przeplatane. Autor argumentuje (i zgadzam się z tym), że ten wynik jest typowy dla używania linq kontra kod imperatywny ... że linq lepiej organizuje twój kod, aby mieć lepszy przepływ między sekcjami.


1 Uważam, że jest to oryginalne źródło: https://twitter.com/mariofusco/status/571999216039542784 . Zauważ również, że ten kod to Java, ale C # byłby podobny.


1
Odroczone wykonanie jest prawdopodobnie największą zaletą iteratorów.
justin.m.chase

12

Czasami sekwencje, które musisz zwrócić, są po prostu zbyt duże, aby zmieścić się w pamięci. Na przykład około 3 miesiące temu brałem udział w projekcie migracji danych pomiędzy bazami MS SLQ. Dane zostały wyeksportowane w formacie XML. Zwrot plonu okazał się całkiem przydatny w XmlReader . To znacznie ułatwiło programowanie. Na przykład załóżmy, że plik zawiera 1000 elementów Customer - jeśli po prostu wczytujesz ten plik do pamięci, będzie to wymagało zapisania ich wszystkich w pamięci jednocześnie, nawet jeśli są obsługiwane sekwencyjnie. Możesz więc używać iteratorów, aby przechodzić przez kolekcję jeden po drugim. W takim przypadku musisz poświęcić tylko pamięć na jeden element.

Jak się okazało, użycie XmlReadera w naszym projekcie było jedynym sposobem na to, aby aplikacja działała - działała przez długi czas, ale przynajmniej nie zawiesiła całego systemu i nie wywołała OutOfMemoryException . Oczywiście możesz pracować z XmlReader bez iteratorów wydajności. Ale iteratory znacznie ułatwiły mi życie (nie napisałbym kodu do importu tak szybko i bez kłopotów). Obejrzyj tę stronę , aby zobaczyć, w jaki sposób iteratory wydajności są używane do rozwiązywania rzeczywistych problemów (nie tylko naukowych z nieskończonymi sekwencjami).


9

W scenariuszach zabawek / demonstracji nie ma dużej różnicy. Ale są sytuacje, w których tworzenie iteratorów jest przydatne - czasami cała lista jest niedostępna (np. Strumienie) lub lista jest kosztowna obliczeniowo i prawdopodobnie nie będzie potrzebna w całości.


2

Jeśli cała lista jest gigantyczna, może pochłonąć dużo pamięci po prostu siedzieć w pobliżu, podczas gdy z wydajnością grasz tylko tym, czego potrzebujesz, kiedy tego potrzebujesz, niezależnie od liczby przedmiotów.



2

Używając yield returnmożesz iterować po elementach bez konieczności tworzenia listy. Jeśli nie potrzebujesz listy, ale chcesz iterować po pewnym zestawie elementów, może być łatwiej napisać

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

Niż

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Możesz umieścić całą logikę do określania, czy chcesz operować na foo wewnątrz swojej metody, używając zwrotów yield, a każda pętla foreach może być znacznie bardziej zwięzła.


2

Oto moja poprzednia zaakceptowana odpowiedź na dokładnie to samo pytanie:

Wartość dodana słowa kluczowego?

Innym sposobem spojrzenia na metody iteracyjne jest to, że wykonują one ciężką pracę odwracania algorytmu „na lewą stronę”. Rozważmy parser. Pobiera tekst ze strumienia, wyszukuje w nim wzorce i generuje logiczny opis zawartości wysokiego poziomu.

Teraz, jako autorowi parsera, mogę sobie to ułatwić, przyjmując podejście SAX, w którym mam interfejs wywołania zwrotnego, który powiadamiam za każdym razem, gdy znajdę następny fragment wzorca. Tak więc w przypadku SAX za każdym razem, gdy znajduję początek elementu, wywołuję beginElementmetodę i tak dalej.

Ale to stwarza problemy dla moich użytkowników. Muszą zaimplementować interfejs programu obsługi, więc muszą napisać klasę maszyny stanu, która będzie odpowiadać na metody wywołania zwrotnego. Trudno to zrobić dobrze, więc najłatwiej jest użyć standardowej implementacji, która buduje drzewo DOM, a wtedy będą mieli wygodę chodzenia po drzewie. Ale potem cała struktura zostaje zapamiętana - niedobrze.

Ale co powiesz na to, że zamiast tego napiszę mój parser jako metodę iteratora?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

Nie będzie to trudniejsze do napisania niż podejście z interfejsem wywołania zwrotnego - po prostu yield zwraca obiekt pochodzący z mojej LanguageElementklasy bazowej zamiast wywoływać metodę callback.

Użytkownik może teraz używać foreach do przechodzenia przez wyjście mojego parsera, dzięki czemu otrzymuje bardzo wygodny, imperatywny interfejs programowania.

W rezultacie obie strony niestandardowego interfejsu API wyglądają tak, jakby miały kontrolę , a zatem są łatwiejsze do napisania i zrozumienia.


2

Podstawowym powodem używania yieldu jest to, że sam generuje / zwraca listę. Możemy użyć zwróconej listy do dalszej iteracji.


Koncepcyjnie poprawne, ale technicznie niepoprawne. Zwraca instancję IEnumerable, która jest po prostu abstrakcją iteratora. Ten iterator jest w rzeczywistości logiką, aby uzyskać następny element, a nie zmaterializowaną listę. Użycie return yieldnie generuje listy, generuje tylko następny element na liście i tylko wtedy, gdy zostanie o to poproszony (iterowany).
Sinaesthetic
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.