Po co używać słowa kluczowego yield, skoro wystarczy zwykły IEnumerable?

171

Biorąc pod uwagę ten kod:

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

Dlaczego nie miałbym tego po prostu zakodować w ten sposób ?:

IEnumerable<object> FilteredList()
{
    var list = new List<object>(); 
    foreach( object item in FullList )
    {
        if( IsItemInPartialList( item ) )
            list.Add(item);
    }
    return list;
}

W pewnym sensie rozumiem, co yieldrobi słowo kluczowe. Mówi kompilatorowi, aby zbudował pewien rodzaj rzeczy (iterator). Ale po co go używać? Poza tym, że jest nieco mniej kodu, co to robi dla mnie?

James P. Wright
źródło
28
Zdaję sobie sprawę, że to tylko przykład, ale tak naprawdę kod powinien być napisany tak: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft

Odpowiedzi:

241

Użycie yieldpowoduje, że kolekcja jest leniwa.

Powiedzmy, że potrzebujesz tylko pierwszych pięciu elementów. Na swój sposób muszę przeglądać całą listę, aby uzyskać pierwsze pięć pozycji. W programie przeglądam yieldtylko pierwsze pięć elementów.

Robert Harvey
źródło
14
Zauważ, że używanie FullList.Where(IsItemInPartialList)będzie równie leniwe. Tylko, że wymaga znacznie mniej niestandardowego kodu wygenerowanego przez kompilator. I mniej czasu programisty na pisanie i konserwację (Oczywiście, że właśnie w tym przykładzie)
sehe
4
To jest Linq, prawda? Wyobrażam sobie, że Linq robi coś bardzo podobnego pod kołdrą.
Robert Harvey
1
Tak, Linq używał opóźnionego wykonywania ( yield return) tam, gdzie było to możliwe.
Chad Schouggins,
11
Nie zapominaj, że jeśli instrukcja yield return nigdy nie zostanie wykonana, nadal otrzymasz pusty wynik kolekcji, więc nie musisz się martwić o wyjątek odwołania o wartości null. zwrot plonów jest niesamowity z posypką czekoladową.
Razor
127

Zaletą bloków iteratorów jest to, że działają leniwie. Możesz więc napisać taką metodę filtrowania:

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
                                   Func<T, bool> predicate)
{
    foreach (var item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

To pozwoli ci filtrować strumień tak długo, jak chcesz, nigdy nie buforując więcej niż jednego elementu na raz. Jeśli na przykład potrzebujesz tylko pierwszej wartości ze zwróconej sekwencji, dlaczego miałbyś chcieć skopiować wszystko do nowej listy?

Jako inny przykład, możesz łatwo utworzyć nieskończony strumień przy użyciu bloków iteratorów. Na przykład, oto sekwencja liczb losowych:

public static IEnumerable<int> RandomSequence(int minInclusive, int maxExclusive)
{
    Random rng = new Random();
    while (true)
    {
        yield return rng.Next(minInclusive, maxExclusive);
    }
}

Jak zapisałbyś nieskończoną sekwencję na liście?

Moja seria blogów Edulinq zawiera przykładową implementację LINQ to Objects, która w dużym stopniu wykorzystuje bloki iteratorów. LINQ jest zasadniczo leniwy tam, gdzie może być - a umieszczanie rzeczy na liście po prostu nie działa w ten sposób.

Jon Skeet
źródło
1
Nie jestem pewien, czy ci się podoba, RandomSequenceczy nie. Dla mnie IEnumerable oznacza - po pierwsze i przede wszystkim - że mogę iterować z foreach, ale to oczywiście doprowadziłoby do nieskończonej pętli. Uważam to za dość niebezpieczne nadużycie koncepcji IEnumerable, ale YMMV.
Sebastian Negraszus
5
@SebastianNegraszus: Sekwencja liczb losowych jest logicznie nieskończona. Możesz na przykład łatwo utworzyć IEnumerable<BigInteger>ciąg reprezentujący ciąg Fibonacciego. Możesz foreachz nim korzystać, ale nic nie IEnumerable<T>gwarantuje, że będzie on ograniczony.
Jon Skeet,
42

Z kodem „listy” musisz przetworzyć pełną listę, zanim będziesz mógł przekazać ją do następnego kroku. Wersja „yield” przenosi przetworzony element natychmiast do następnego kroku. Jeśli ten „następny krok” zawiera „.Take (10)”, to wersja „yield” przetworzy tylko pierwsze 10 elementów i zapomni o reszcie. Kod „listy” przetworzyłby wszystko.

Oznacza to, że największą różnicę widać, gdy trzeba dużo przetworzyć i / lub mieć długie listy elementów do przetworzenia.

Hans Ke ing
źródło
23

Możesz użyć, yieldaby zwrócić elementy, których nie ma na liście. Oto mała próbka, która może powtarzać się w nieskończoność po liście, dopóki nie zostanie anulowana.

public IEnumerable<int> GetNextNumber()
{
    while (true)
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
    }
}

public bool Canceled { get; set; }

public void StartCounting()
{
    foreach (var number in GetNextNumber())
    {
        if (this.Canceled) break;
        Console.WriteLine(number);
    }
}

To pisze

0
1
2
3
4
5
6
7
8
9
0
1
2
3
4

...itp. do konsoli do czasu anulowania.

Jason Whitted
źródło
10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Gdy powyższy kod jest używany do pętli przez FilteredList () i zakładając, że item.Name == "James" będzie spełniony na 2. pozycji na liście, metoda wykorzystująca yield zwróci dwa razy. To jest leniwe zachowanie.

Gdzie jako metoda używająca list doda wszystkie n obiektów do listy i przekaże pełną listę do metody wywołującej.

To jest dokładnie przypadek użycia, w którym można wyróżnić różnicę między IEnumerable i IList.

humblelistener
źródło
7

Najlepszym przykładem użycia programu w świecie rzeczywistym, jaki widziałem, yieldbyłoby obliczenie ciągu Fibonacciego.

Rozważ następujący kod:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(string.Join(", ", Fibonacci().Take(10)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(15).Take(1)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(10).Take(5)));
        Console.WriteLine(string.Join(", ", Fibonacci().Skip(100).Take(1)));
        Console.ReadKey();
    }

    private static IEnumerable<long> Fibonacci()
    {
        long a = 0;
        long b = 1;

        while (true)
        {
            long temp = a;
            a = b;

            yield return a;

            b = temp + b;
        }
    }
}

To zwróci:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55
987
89, 144, 233, 377, 610
1298777728820984005

Jest to przyjemne, ponieważ pozwala szybko i łatwo obliczyć nieskończoną serię, dając możliwość korzystania z rozszerzeń Linq i przeszukiwania tylko tego, czego potrzebujesz.

Middas
źródło
5
Nie widzę niczego „prawdziwego świata” w obliczeniach sekwencji Fibonacciego.
Nek
Zgadzam się, że to naprawdę nie jest „prawdziwy świat”, ale co za fajny pomysł.
Casey,
1

po co używać [yield]? Poza tym, że jest nieco mniej kodu, co to dla mnie robi?

Czasami jest to przydatne, czasami nie. Jeśli cały zestaw danych musi zostać zbadany i zwrócony, nie będzie żadnych korzyści z używania wydajności, ponieważ wszystko, co zrobił, to wprowadzenie narzutu.

Kiedy wydajność naprawdę świeci, jest zwracana tylko część zestawu. Myślę, że najlepszym przykładem jest sortowanie. Załóżmy, że masz listę obiektów zawierającą datę i kwotę w dolarach z tego roku i chciałbyś zobaczyć pierwszą garść (5) rekordów z tego roku.

Aby to osiągnąć, listę należy posortować rosnąco według daty, a następnie wziąć pierwszych 5. Jeśli zrobiono to bez plonów, całość lista musiałaby zostać posortowana, aż do upewnienia się, że ostatnie dwie daty są w porządku.

Jednak z wydajnością po ustaleniu pierwszych 5 pozycji sortowanie zatrzymuje się i wyniki są dostępne. Może to zaoszczędzić dużo czasu.

Travis J.
źródło
0

Instrukcja zwrotu zysku umożliwia zwrócenie tylko jednej pozycji naraz. Zbierasz wszystkie elementy na liście i ponownie zwracasz tę listę, co jest narzutem pamięci.

Prabhavith
źródło