Kiedy nie używać yield (return) [duplikat]

To pytanie ma już odpowiedź tutaj:
czy istnieje kiedykolwiek powód, aby nie używać "yield return" przy zwracaniu liczby mnogiej?

Istnieje kilka przydatnych pytań tutaj na temat SO o korzyściach yield return. Na przykład,

Szukam myśli, Kiedy NIE użyć yield return. Na przykład, jeśli spodziewam się, że będę musiał zwrócić wszystkie przedmioty w kolekcji, nie wydaje się to jak yield byłoby przydatne, prawda?

Jakie są przypadki, w których użycie yield będzie ograniczać, niepotrzebne, wpędzić mnie w kłopoty, lub w inny sposób należy unikać?

Author: Community, 2010-10-19

11 answers

Jakie są przypadki, w których użycie plonu będzie ograniczać, niepotrzebne, wpędzić mnie w kłopoty, lub w inny sposób należy unikać?

Dobrym pomysłem jest uważne zastanowienie się nad używaniem "yield return" podczas radzenia sobie z rekurencyjnie zdefiniowanymi strukturami. Na przykład często widzę to:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    if (root == null) yield break;
    yield return root.Value;
    foreach(T item in PreorderTraversal(root.Left))
        yield return item;
    foreach(T item in PreorderTraversal(root.Right))
        yield return item;
}

Doskonale wyglądający kod, ale ma problemy z wydajnością. Przypuśćmy, że drzewo jest głębokie. Wtedy w większości punktów będą wbudowane Iteratory zagnieżdżone O (h). Wywołanie " MoveNext" na zewnętrznym iteratorze wykonają zagnieżdżone wywołania O (h) do MoveNext. Ponieważ robi to O(N) razy dla drzewa z N pozycji, to sprawia, że algorytm O (hn). A ponieważ wysokość drzewa binarnego wynosi lg n

Ale iteracja drzewa może być O (n) w czasie I O(1) w przestrzeni stosu. Możesz to napisać w następujący sposób:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    var stack = new Stack<Tree<T>>();
    stack.Push(root);
    while (stack.Count != 0)
    {
        var current = stack.Pop();
        if (current == null) continue;
        yield return current.Value;
        stack.Push(current.Left);
        stack.Push(current.Right);
    }
}

Który nadal używa zwrotu plonów, ale jest o wiele mądrzejszy. Teraz jesteśmy O (n) w czasie I O(h) w przestrzeni sterty, I O(1) w przestrzeni stosu.

Czytaj dalej: zobacz artykuł Wesa Dyera na ten temat:

Http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx

 131
Author: Eric Lippert,
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
2010-10-19 19:01:34

Jakie są przypadki, w których użycie plonu będzie ograniczać, niepotrzebnie, dostać mnie w kłopoty, lub w inny sposób powinny być uniknął?

Myślę o kilku przypadkach, czyli:

  • Unikaj używania yield return, gdy zwracasz istniejący iterator. Przykład:

    // Don't do this, it creates overhead for no reason
    // (a new state machine needs to be generated)
    public IEnumerable<string> GetKeys() 
    {
        foreach(string key in _someDictionary.Keys)
            yield return key;
    }
    // DO this
    public IEnumerable<string> GetKeys() 
    {
        return _someDictionary.Keys;
    }
    
  • Unikaj używania yield return, gdy nie chcesz odkładać kodu wykonania dla metody. Przykład:

    // Don't do this, the exception won't get thrown until the iterator is
    // iterated, which can be very far away from this method invocation
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         yield ...
    }
    // DO this
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         return new BazIterator(baz);
    }
    
 55
Author: Pop Catalin,
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-11-15 10:20:29

Kluczową rzeczą do zrozumienia jest to, do czego yield jest przydatna, wtedy możesz zdecydować, które przypadki z tego nie korzystają.

Innymi słowy, jeśli nie potrzebujesz sekwencji do leniwej oceny, możesz pominąć użycie yield. Kiedy to będzie? Byłoby tak, gdy nie masz nic przeciwko natychmiastowemu zapamiętywaniu całej kolekcji. W przeciwnym razie, jeśli masz ogromną sekwencję, która negatywnie wpłynęłaby na pamięć, chciałbyś użyć yield, aby pracować nad nią krok po kroku (np. leniwie). Profiler może się przydać przy porównywaniu obu podejść.

Zauważ jak większość poleceń LINQ zwraca IEnumerable<T>. Pozwala nam to na ciągłe łączenie różnych operacji LINQ bez negatywnego wpływu na wydajność na każdym kroku (aka deferred execution). Alternatywnym obrazem byłoby umieszczenie wywołania ToList() pomiędzy każdą instrukcją LINQ. Spowoduje to, że każde poprzednie polecenie LINQ zostanie natychmiast wykonane przed wykonaniem następnego (przykutego) polecenia LINQ, rezygnując w ten sposób z jakichkolwiek korzyści z leniwej oceny i wykorzystania IEnumerable<T> till potrzebne.

 30
Author: Ahmad Mageed,
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
2010-10-19 15:39:38

Istnieje wiele doskonałych odpowiedzi tutaj. Dodam jeszcze jedno: nie używaj zwrotu yield dla małych lub pustych zbiorów, w których znasz już wartości:

IEnumerable<UserRight> GetSuperUserRights() {
    if(SuperUsersAllowed) {
        yield return UserRight.Add;
        yield return UserRight.Edit;
        yield return UserRight.Remove;
    }
}

W tych przypadkach tworzenie obiektu Enumerator jest droższe i bardziej wyraziste niż generowanie struktury danych.

IEnumerable<UserRight> GetSuperUserRights() {
    return SuperUsersAllowed
           ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove}
           : Enumerable.Empty<UserRight>();
}

Update

Oto wyniki mój benchmark :

Wyniki Porównawcze

Te wyniki pokazują, ile czasu zajęło (w milisekundach) wykonanie operacja 1,000,000 razy. Mniejsze liczby są lepsze.

Wracając do tego, różnica w wydajności nie jest wystarczająco znacząca, aby się martwić, więc powinieneś wybrać to, co najłatwiejsze do odczytania i utrzymania.

Update 2

Jestem prawie pewien, że powyższe wyniki zostały osiągnięte z wyłączoną optymalizacją kompilatora. Działa w trybie Release z nowoczesnym kompilatorem, wydaje się, że wydajność jest praktycznie nie do odróżnienia między tymi dwoma. Idź z tym, co jest najbardziej czytelne za Ciebie.

 22
Author: StriplingWarrior,
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-10-05 15:28:22

Eric Lippert podnosi dobry punkt (szkoda, że C# nie ma stream flatning jak Cw ). Dodam, że czasami proces wyliczania jest kosztowny z innych powodów, dlatego powinieneś użyć listy, jeśli zamierzasz powtarzać liczbę razy więcej niż raz.

Na przykład, LINQ-to-objects jest zbudowane na "yield return". Jeśli napisałeś powolne zapytanie LINQ (np. filtrujące dużą listę w małą listę lub sortujące i grupujące), może być rozsądne wywołanie ToList() Na wyniku zapytania w celu uniknięcia wielokrotnego wyliczania (które faktycznie wykonuje zapytanie wielokrotnie).

Jeśli podczas pisania metody wybierasz pomiędzy "yield return" a List<T>, zastanów się: czy jest to kosztowne i czy rozmówca będzie musiał wyliczyć wyniki więcej niż raz? Jeśli wiesz, że odpowiedź brzmi tak, to nie używaj "yield return", chyba że produkowana lista jest bardzo duża (i nie stać Cię na pamięć, której by używała-pamiętaj, kolejna zaleta yield jest to, że lista wyników nie musi być całkowicie w pamięci na raz).

Innym powodem, aby nie używać "yield return" jest to, że operacje przeplatania są niebezpieczne. Na przykład, jeśli twoja metoda wygląda mniej więcej tak,

IEnumerable<T> GetMyStuff() {
    foreach (var x in MyCollection)
        if (...)
            yield return (...);
}

Jest to niebezpieczne, jeśli istnieje szansa, że MyCollection zmieni się z powodu czegoś, co robi rozmówca:

foreach(T x in GetMyStuff()) {
    if (...)
        MyCollection.Add(...);
        // Oops, now GetMyStuff() will throw an exception because
        // MyCollection was modified.
}

yield return może powodować problemy, gdy wywołujący zmienia coś, co zakłada, że funkcja nie zmienia się.

 16
Author: Qwertie,
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
2010-10-19 16:57:53

Wydajność byłaby ograniczona / niepotrzebna, gdy potrzebujesz dostępu losowego. Jeśli potrzebujesz dostępu do elementu 0, a następnie elementu 99, prawie wyeliminowałeś przydatność leniwej oceny.

 5
Author: Robert Gowland,
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
2010-10-19 15:35:32

Jednym z nich może cię złapać, jeśli porządkujesz wyniki wyliczenia i wysyłasz je przez przewód. Ponieważ wykonanie jest odroczone, dopóki wyniki nie będą potrzebne, będziesz serializował puste wyliczenie i odeślij je z powrotem zamiast żądanych wyników.

 5
Author: Aidan,
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
2010-10-19 15:44:27

Uniknąłbym używania yield return jeśli metoda ma efekt uboczny, którego można się spodziewać po wywołaniu metody. Jest to spowodowane odroczoną egzekucją, o której wspomina Pop Catalin .

Jednym z efektów ubocznych może być modyfikacja systemu, co może się zdarzyć w metodzie takiej jak IEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos(), która łamie zasadę pojedynczej odpowiedzialności . To dość oczywiste (teraz...), ale nie tak oczywistym efektem ubocznym może być ustawienie wyniku buforowanego lub podobnego jako optymalizacja.

Moje zasady (ponownie, teraz...) są:

  • używaj tylko yield jeśli zwracany obiekt wymaga trochę przetworzenia
  • brak skutków ubocznych w metodzie, jeśli muszę użyć yield
  • jeśli trzeba mieć skutki uboczne (i ograniczenie tego do buforowania itp.), nie używaj yield i upewnij się, że korzyści z rozszerzenia iteracji przewyższają koszty
 4
Author: Ben Scott,
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-05-23 11:55:10

Muszę utrzymać stos kodu od faceta, który miał obsesję na punkcie zwrotu plonów i był liczniejszy. Problem polega na tym, że wiele interfejsów API innych firm, których używamy, a także wiele naszego własnego kodu, zależy od List lub tablic. Więc kończę to robić:

IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());

Niekoniecznie złe, ale trochę denerwujące, a przy kilku okazjach prowadzi to do tworzenia zduplikowanych list w pamięci, aby uniknąć refaktoryzacji wszystkiego.

 2
Author: Mike Ruhlin,
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
2010-10-19 16:04:48

Jeśli nie chcesz, aby blok kodu zwrócił iterator dla sekwencyjnego dostępu do bazowej kolekcji, nie potrzebujesz yield return. Po prostu return zbiór.

 1
Author: explorer,
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
2010-10-19 15:38:56

Jeśli definiujesz metodę rozszerzenia Linq-y, w której owijasz rzeczywiste elementy Linq, elementy te częściej niż nie zwracają iteratora. Poddanie się przez iterator jest niepotrzebne.

Poza tym, nie możesz mieć większych problemów, używając yield do zdefiniowania" strumieniowego " wyliczenia, które jest oceniane na podstawie JIT.

 0
Author: KeithS,
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
2010-10-19 15:36:55