Właściwe stosowanie „zwrotu z zysku”

903

Wydajność słów kluczowych jest jednym z tych słów kluczowych w języku C #, który nadal mnie zagadką, a ja nigdy nie byłem przekonany, że używam go poprawnie.

Który z poniższych dwóch fragmentów kodu jest preferowany i dlaczego?

Wersja 1: Korzystanie ze zwrotu z zysków

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Wersja 2: zwraca listę

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}
senfo
źródło
38
yieldjest przywiązany IEnumerable<T>i tego rodzaju. Jest to w jakiś sposób leniwa ocena
Jaider 20.12.12
oto uprzejma odpowiedź na podobne pytanie. stackoverflow.com/questions/15381708/…
Sanjeev Rai
1
Oto dobry przykład użycia: stackoverflow.com/questions/3392612/…
ValGe
6
Widzę dobry przypadek użycia, yield returnjeśli kod, który iteruje wyniki, GetAllProducts()pozwala użytkownikowi przedwcześnie anulować przetwarzanie.
JMD
2
Znalazłem ten wątek bardzo pomocny: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Odpowiedzi:

806

Zazwyczaj używam zwrotu z zysku, gdy obliczam następny element na liście (lub nawet następną grupę elementów).

Korzystając z wersji 2, musisz mieć pełną listę przed powrotem. Korzystając ze zwrotu z inwestycji, naprawdę musisz mieć tylko następny przedmiot przed zwrotem.

Pomaga to między innymi rozłożyć koszt obliczeniowy skomplikowanych obliczeń w dłuższych ramach czasowych. Na przykład, jeśli lista jest podłączona do GUI, a użytkownik nigdy nie przechodzi do ostatniej strony, nigdy nie oblicza się końcowych pozycji na liście.

Innym przypadkiem, w którym preferowany jest zwrot z zysku, jest to, że IEnumerable reprezentuje zestaw nieskończony. Rozważ listę liczb pierwszych lub nieskończoną listę liczb losowych. Nigdy nie możesz zwrócić pełnego IEnumerable na raz, więc używasz return-return, aby zwracać listę przyrostowo.

W twoim przykładzie masz pełną listę produktów, więc użyłbym wersji 2.

Abelenky
źródło
31
Chciałbym zauważyć, że w twoim przykładzie pytanie 3 łączy dwie korzyści. 1) Rozdziela koszt obliczeniowy (czasem korzyść, czasem nie) 2) W wielu przypadkach może leniwie uniknąć obliczeń w nieskończoność. Nie wspomina się o potencjalnej wadzie, jaką utrzymuje w stanie pośrednim. Jeśli masz znaczną ilość stanu pośredniego (powiedz HashSet w celu zduplikowania eliminacji), wówczas użycie wydajności może zawyżać twój ślad pamięci.
Kennet Belenky,
8
Ponadto, jeśli każdy pojedynczy element jest bardzo duży, ale dostęp do nich wymaga tylko sekwencyjnego, wydajność jest lepsza.
Kennet Belenky,
2
I na koniec ... jest nieco niepewna, ale czasami skuteczna technika wykorzystywania plonu do pisania kodu asynchronicznego w bardzo serializowanej formie.
Kennet Belenky,
12
Innym przykładem, który może być interesujący, jest czytanie raczej dużych plików CSV. Chcesz przeczytać każdy element, ale chcesz też usunąć swoją zależność. Wydajność zwracająca IEnumerable <> pozwoli ci zwrócić każdy wiersz i przetworzyć każdy wiersz osobno. Nie ma potrzeby wczytywania pliku 10 Mb do pamięci. Tylko jedna linia na raz.
Maxime Rouiller
1
Yield returnwydaje się być skrótem do pisania własnej niestandardowej klasy iteratora (zaimplementuj IEnumerator). Dlatego wspomniane korzyści dotyczą również niestandardowych klas iteratorów. W każdym razie obie konstrukcje zachowują stan pośredni. W najprostszej postaci chodzi o trzymanie odniesienia do bieżącego obiektu.
J. Ouwehand,
641

Wypełnianie listy tymczasowej jest jak pobieranie całego wideo, podczas gdy używanie yieldjest jak przesyłanie strumieniowe tego wideo.

anar khalilov
źródło
180
Doskonale zdaję sobie sprawę, że ta odpowiedź nie jest odpowiedzią techniczną, ale uważam, że podobieństwo między wydajnością a strumieniowaniem wideo stanowi dobry przykład w zrozumieniu słowa kluczowego wydajności. Wszystko techniczne zostało już powiedziane na ten temat, więc próbowałem wyjaśnić „innymi słowy”. Czy istnieje zasada wspólnoty, która mówi, że nie możesz wyjaśniać swoich pomysłów w kategoriach nietechnicznych?
anar khalilov
13
Nie jestem pewien, kto cię zagłosował lub dlaczego (chciałbym, żeby to skomentowali), ale myślę, że to w pewien sposób opisuje to z nietechnicznej perspektywy.
senfo
22
Wciąż chwytając się koncepcji, pomogło to skupić się na niej, ładna analogia.
Tony
11
Podoba mi się ta odpowiedź, ale nie odpowiada na pytanie.
ANeves
73

Jako koncepcyjny przykład zrozumienia, kiedy powinieneś użyć yield, powiedzmy, że metoda ConsumeLoop()przetwarza elementy zwrócone / uzyskane przez ProduceList():

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Bez yieldtego połączenie ProduceList()może zająć dużo czasu, ponieważ musisz wypełnić listę przed zwróceniem:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

Za pomocą yieldzmienia się, działa jakby „równolegle”:

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

I wreszcie, jak wielu wcześniej sugerowało, powinieneś użyć wersji 2, ponieważ i tak masz już ukończoną listę.

Kache
źródło
30

Wiem, że to stare pytanie, ale chciałbym podać jeden przykład, w jaki sposób można twórczo wykorzystać słowo kluczowe wydajności. I rzeczywiście skorzystało z tej techniki. Mam nadzieję, że będzie to pomocne dla każdego, kto natknie się na to pytanie.

Uwaga: nie myśl o słowu kluczowym wydajności jako o innym sposobie budowania kolekcji. Duża część siły dochodu polega na tym, że wykonanie jest wstrzymane w metodzie lub właściwości, dopóki kod wywołujący nie przejdzie do następnej wartości. Oto mój przykład:

Użycie słowa kluczowego fed (wraz z implementacją Caliburn.Micro Coroutines Roba Eisenburga ) pozwala mi wyrazić asynchroniczne wywołanie usługi internetowej:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Spowoduje to włączenie mojego BusyIndicator, wywołanie metody Login w mojej usłudze internetowej, ustawienie flagi IsLoggedIn na wartość zwracaną, a następnie wyłączenie BusyIndicator.

Oto jak to działa: IResult ma metodę wykonania i zdarzenie zakończone. Caliburn.Micro pobiera IEnumerator z wywołania HandleButtonClick () i przekazuje go do metody Coroutine.BeginExecute. Metoda BeginExecute rozpoczyna iterację poprzez IResults. Kiedy zwracany jest pierwszy IResult, wykonywanie jest wstrzymywane w HandleButtonClick (), a BeginExecute () dołącza moduł obsługi zdarzenia do zdarzenia Completed i wywołuje Execute (). IResult.Execute () może wykonać zadanie synchroniczne lub asynchroniczne i po zakończeniu uruchamia zdarzenie Completed.

LoginResult wygląda mniej więcej tak:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Może pomóc skonfigurować coś takiego i przejść przez wykonanie, aby zobaczyć, co się dzieje.

Mam nadzieję, że to komuś pomoże! Naprawdę podobało mi się odkrywanie różnych sposobów wykorzystania plonu.

Adam W. McKinley
źródło
1
Twój przykładowy kod jest doskonałym przykładem tego, jak używać plonu ZEWNĄTRZ bloku for lub foreach. Większość przykładów pokazuje zwrot z zysku w iteratorze. Bardzo pomocne, ponieważ miałem właśnie zadać pytanie na temat SO Jak korzystać z plonu poza iteratorem!
shelbypereira
Nigdy nie przyszło mi do głowy, aby używać yieldw ten sposób. Wydaje się to być eleganckim sposobem na emulację wzorca asynchronizacji / oczekiwania (który, jak zakładam, zostałby użyty zamiast, yieldgdyby został przepisany dzisiaj). Czy odkryłeś, że te twórcze zastosowania yieldprzyniosły (bez zamierzonej gry słów) malejące zwroty w miarę upływu lat, gdy C # ewoluował od czasu odpowiedzi na to pytanie? A może wciąż masz zmodernizowane sprytne przypadki użycia, takie jak ten? A jeśli tak, czy mógłbyś podzielić się z nami kolejnym interesującym scenariuszem?
Lopsided
27

To może wydawać się dziwną sugestią, ale nauczyłem się używać yieldsłowa kluczowego w C #, czytając prezentację na temat generatorów w Pythonie: David M. Beazley's http://www.dabeaz.com/generators/Generators.pdf . Nie musisz znać dużo języka Python, aby zrozumieć prezentację - ja nie. Uznałem, że jest to bardzo pomocne w wyjaśnieniu nie tylko tego, jak działają generatory, ale dlaczego warto się tym przejmować.

Robert Rossney
źródło
1
Prezentacja zapewnia prosty przegląd. Szczegóły działania w języku C # zostały omówione przez Ray Chen w linkach na stackoverflow.com/a/39507/939250 . Pierwsze łącze szczegółowo wyjaśnia, że ​​istnieje drugi, niejawny zwrot na końcu metod zwrotu z zysku.
Donal Lafferty,
18

Zwrot wydajności może być bardzo silny w przypadku algorytmów, w których trzeba iterować przez miliony obiektów. Rozważ następujący przykład, w którym musisz obliczyć możliwe przejazdy w celu współdzielenia jazdy. Najpierw generujemy możliwe wycieczki:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Następnie powtarzaj każdą podróż:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Jeśli użyjesz List zamiast wydajności, będziesz musiał przydzielić 1 milion obiektów do pamięci (~ 190mb), a ten prosty przykład zajmie ~ 1400ms. Jeśli jednak użyjesz wydajności, nie musisz umieszczać wszystkich tych obiektów tymczasowych w pamięci, a uzyskasz znacznie większą szybkość algorytmu: ten przykład zajmie tylko ~ 400 ms bez użycia pamięci.

Andzej Maciusovic
źródło
2
co to jest pod przykryciem? Myślałbym, że to lista, a więc jak poprawiłaby wykorzystanie pamięci?
rzuca
1
@rolls yielddziała pod przykryciem, implementując wewnętrznie maszynę stanu. Oto odpowiedź SO z 3 szczegółowymi postami na blogu MSDN, które szczegółowo wyjaśniają wdrożenie. Wpisany przez Raymond Chen @ MSFT
Shiva
13

Dwa fragmenty kodu naprawdę robią dwie różne rzeczy. Pierwsza wersja pobierze członków tak, jak ich potrzebujesz. Druga wersja załaduje wszystkie wyniki do pamięci, zanim zaczniesz z nią cokolwiek robić.

Nie ma dobrej lub złej odpowiedzi na to pytanie. Który z nich jest lepszy, zależy tylko od sytuacji. Na przykład, jeśli istnieje limit czasu na wypełnienie zapytania i musisz zrobić coś częściowo skomplikowanego z wynikami, druga wersja może być lepsza. Uważaj jednak na duże zestawy wyników, zwłaszcza jeśli uruchamiasz ten kod w trybie 32-bitowym. Kilka razy zostałem ugryziony przez wyjątki OutOfMemory podczas wykonywania tej metody.

Kluczową rzeczą, o której należy pamiętać, jest to: różnice w wydajności. Dlatego prawdopodobnie powinieneś wybrać ten, który upraszcza kod i zmienić go dopiero po profilowaniu.

Jason Baker
źródło
11

Wydajność ma dwa świetne zastosowania

Pomaga zapewnić niestandardową iterację bez tworzenia kolekcji tymczasowych. (ładowanie wszystkich danych i zapętlanie)

Pomaga wykonać stanową iterację. (streaming)

Poniżej znajduje się prosty film, który stworzyłem z pełną demonstracją w celu wsparcia powyższych dwóch punktów

http://www.youtube.com/watch?v=4fju3xcm21M

Shivprasad Koirala
źródło
10

Oto, co Chris Sells mówi o tych wypowiedziach w języku programowania C # ;

Czasami zapominam, że zwrot z zysku nie jest tym samym, co zwrot, ponieważ kod po zakończeniu zwrotu może być wykonany. Na przykład kod po pierwszym powrocie tutaj nigdy nie może zostać wykonany:

    int F() {
return 1;
return 2; // Can never be executed
}

Natomiast kod po pierwszym zwrocie wydajności można tutaj wykonać:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

To często gryzie mnie w stwierdzeniu if:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

W takich przypadkach pomocne jest zapamiętanie, że zwrot z zysku nie jest „ostateczny”, podobnie jak zwrot.

Teoman Shipahi
źródło
aby ograniczyć dwuznaczność, proszę wyjaśnić, kiedy mówisz „może”, „czy”, czy „może”? czy może być możliwe, że pierwszy zwróci i nie wykona drugiego zwrotu?
Johno Crawford,
@JohnoCrawford druga instrukcja wydajności zostanie wykonana tylko wtedy, gdy zostanie wyliczona druga / następna wartość IEnumerable. Jest całkiem możliwe, że tak się nie stanie, np. F().Any()- wróci po próbie wyliczenia tylko pierwszego wyniku. Ogólnie rzecz biorąc, nie powinieneś polegać na IEnumerable yieldzmianie stanu programu, ponieważ może on nie zostać uruchomiony
Zac Faragher
8

Zakładając, że twoje produkty klasa LINQ używa podobnej wydajności do wyliczania / iteracji, pierwsza wersja jest bardziej wydajna, ponieważ daje tylko jedną wartość za każdym razem, gdy jest iterowana.

Drugi przykład to konwersja modułu wyliczającego / iteratora na listę za pomocą metody ToList (). Oznacza to, że ręcznie iteruje wszystkie elementy modułu wyliczającego, a następnie zwraca płaską listę.

Soviut
źródło
8

Jest to trochę poza tym, ale ponieważ pytanie jest oznaczone najlepszymi praktykami, pójdę dalej i wrzucę moje dwa centy. W przypadku tego typu rzeczy zdecydowanie wolę przekształcić je we właściwość:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Jasne, to trochę więcej płyt kotłowych, ale kod, który tego używa, będzie wyglądał znacznie lepiej:

prices = Whatever.AllProducts.Select (product => product.price);

vs

prices = Whatever.GetAllProducts().Select (product => product.price);

Uwaga: nie zrobiłbym tego w przypadku metod, które mogłyby zająć trochę czasu, aby wykonać swoją pracę.

Mark A. Nicolosi
źródło
7

A co z tym?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Myślę, że to jest o wiele czystsze. Nie mam jednak pod ręką VS2008 do sprawdzenia. W każdym razie, jeśli Produkty implementują IEnumerable (jak się wydaje - jest używana w instrukcji foreach), zwrócę to bezpośrednio.

petr k.
źródło
2
Edytuj OP, aby dodać więcej informacji zamiast publikowania odpowiedzi.
Brian Rasmussen
Cóż, musisz mi powiedzieć, co dokładnie oznacza OP :-) Dzięki
petr k.
Zakładam, że oryginalny post. Nie mogę edytować postów, więc wydawało się, że jest to właściwa droga.
petr k.
5

W tym przypadku użyłbym wersji 2 kodu. Ponieważ dostępna jest pełna lista produktów i tego właśnie oczekuje „konsument” wywołania tej metody, konieczne będzie odesłanie pełnej informacji z powrotem do osoby dzwoniącej.

Jeśli osoba wywołująca tę metodę wymaga „jednej” informacji na raz, a zużycie następnej informacji odbywa się na żądanie, wówczas korzystne byłoby zastosowanie zwrotu z zysku, który zapewni, że polecenie wykonania zostanie zwrócone do osoby dzwoniącej, gdy dostępna jest jednostka informacji.

Oto kilka przykładów, w których można zastosować zwrot z zysków:

  1. Skomplikowane obliczenia krok po kroku, w których dzwoniący oczekuje na dane krok po kroku
  2. Stronicowanie w GUI - użytkownik może nigdy nie dotrzeć do ostatniej strony, a na bieżącej stronie wymagany jest tylko podzestaw informacji

Aby odpowiedzieć na twoje pytania, użyłbym wersji 2.

IntelligentBinary
źródło
3

Zwróć listę bezpośrednio. Korzyści:

  • To jest bardziej jasne
  • Lista jest wielokrotnego użytku. (iterator nie jest) nie jest prawdą, dzięki Jon

Powinieneś używać iteratora (wydajności) od momentu, gdy uważasz, że prawdopodobnie nie będziesz musiał iterować aż do końca listy lub gdy nie ma ona końca. Na przykład, wywołanie klienta będzie szukało pierwszego produktu, który spełnia niektóre predykaty, możesz rozważyć użycie iteratora, chociaż jest to wymyślony przykład i są prawdopodobnie lepsze sposoby na osiągnięcie tego. Zasadniczo, jeśli wiesz z góry, że trzeba będzie obliczyć całą listę, po prostu zrób to z góry. Jeśli uważasz, że tak nie będzie, rozważ użycie wersji iteratora.

rekurencyjny
źródło
Nie zapominaj, że zwraca w IEnumerable <T>, a nie IEnumerator <T> - możesz ponownie wywołać GetEnumerator.
Jon Skeet
Nawet jeśli wiesz z góry, że trzeba będzie obliczyć całą listę, nadal korzystne może być zastosowanie zwrotu z zysku. Jednym z przykładów jest sytuacja, gdy kolekcja zawiera setki tysięcy przedmiotów.
Val
1

Keyphrase zwrotu wydajności służy do utrzymywania automatu stanów dla określonej kolekcji. Ilekroć CLR widzi używaną frazę zwrotu zysku, CLR implementuje wzorzec modułu wyliczającego do tego fragmentu kodu. Ten rodzaj implementacji pomaga programistom we wszystkich instalacjach hydraulicznych, które w innym przypadku musielibyśmy zrobić bez słowa kluczowego.

Załóżmy, że programista filtruje jakąś kolekcję, iteruje kolekcję, a następnie wyodrębnia te obiekty w nowej kolekcji. Tego rodzaju hydraulika jest dość monotonna.

Więcej informacji o tym słowie kluczowym znajduje się w tym artykule .

Vikram
źródło
-4

Wykorzystanie wydajności jest podobne do słowa kluczowego return , z tym wyjątkiem, że zwróci generator . A generator obiekt będzie przechodzić tylko raz .

plon ma dwie zalety:

  1. Nie musisz czytać tych wartości dwa razy;
  2. Możesz pobrać wiele węzłów potomnych, ale nie musisz zapisywać ich wszystkich w pamięci.

Istnieje inne jasne wytłumaczenie, które może ci pomóc.

123
źródło