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 return
składni w porównaniu z zwracaniem listy?
Ponadto w jakich scenariuszach można yield return
by użyć, że nie można po prostu zwrócić pełnej listy?
c#
iterator
yield-return
CoderDennis
źródło
źródło
yield
s jest to, że nie musisz nazywać kolejnej zmiennej pośredniej.Odpowiedzi:
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.Range
metoda 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.
źródło
Dobre odpowiedzi sugerują, że zaletą
yield return
jest 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 return
umoż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; }
źródło
yield!
sposób, w jaki robi to F #, aby nie były potrzebne wszystkieforeach
instrukcje.yield return
: często nie jest oczywiste, kiedy wygeneruje wydajny lub nieefektywny kod. Chociażyield return
moż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.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
foreach
pę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 :
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.
źródło
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).
źródło
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.
źródło
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.
źródło
Spójrz na tę dyskusję na blogu Erica White'a (nawiasem mówiąc, świetny blog) na temat oceny leniwej i gorliwej .
źródło
Używając
yield return
moż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.
źródło
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ę
beginElement
metodę 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
LanguageElement
klasy 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.
źródło
Podstawowym powodem używania yieldu jest to, że sam generuje / zwraca listę. Możemy użyć zwróconej listy do dalszej iteracji.
źródło
return yield
nie generuje listy, generuje tylko następny element na liście i tylko wtedy, gdy zostanie o to poproszony (iterowany).