Mengapa menggunakan kata kunci hasil, ketika saya bisa menggunakan IEnumerable biasa?

171

Diberikan kode ini:

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

Mengapa saya tidak harus hanya kode seperti ini ?:

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

Saya agak mengerti apa yang dilakukan yieldkata kunci. Ini memberitahu kompiler untuk membangun hal tertentu (sebuah iterator). Tapi mengapa menggunakannya? Terlepas dari itu kode yang sedikit kurang, apa fungsinya bagi saya?

James P. Wright
sumber
28
Saya menyadari ini hanya sebuah contoh, tetapi sungguh, kode harus ditulis seperti ini: FullList.Where(IsItemInPartialList):)
BlueRaja - Danny Pflughoeft
Posting terkait - Untuk apa kata kunci hasil dalam C #?
RBT

Jawaban:

241

Menggunakan yieldmembuat koleksi malas.

Katakanlah Anda hanya membutuhkan lima item pertama. Cara Anda, saya harus mengulang seluruh daftar untuk mendapatkan lima item pertama. Dengan yield, saya hanya mengulang lima item pertama.

Robert Harvey
sumber
14
Perhatikan bahwa menggunakan FullList.Where(IsItemInPartialList)akan sama malasnya. Hanya saja, ia membutuhkan jauh lebih sedikit kompiler yang dihasilkan kode kustom --- gunk ---. Dan kurang waktu pengembang menulis dan memelihara. (Tentu saja, itu hanya contoh ini)
lihat
4
Itu Linq, bukan? Saya membayangkan Linq melakukan sesuatu yang sangat mirip di bawah selimut.
Robert Harvey
1
Ya, Linq menggunakan eksekusi yang tertunda ( yield return) jika memungkinkan.
Chad Schouggins
11
Jangan lupa bahwa jika pernyataan pengembalian hasil tidak pernah dijalankan, Anda masih mendapatkan hasil pengumpulan kosong sehingga tidak perlu khawatir tentang pengecualian referensi nol. imbal hasil luar biasa dengan taburan cokelat.
Razor
127

Keuntungan dari blok iterator adalah mereka bekerja dengan malas. Jadi, Anda dapat menulis metode pemfilteran seperti ini:

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;
        }
    }
}

Itu akan memungkinkan Anda untuk menyaring aliran selama Anda suka, tidak pernah buffer lebih dari satu item pada suatu waktu. Jika Anda hanya membutuhkan nilai pertama dari urutan yang dikembalikan, misalnya, mengapa Anda ingin menyalin semuanya ke daftar baru?

Sebagai contoh lain, Anda dapat dengan mudah membuat aliran tanpa batas menggunakan blok iterator. Misalnya, inilah urutan angka acak:

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

Bagaimana Anda menyimpan urutan yang tak terbatas dalam daftar?

Seri blog Edulinq saya memberikan contoh implementasi LINQ ke Objek yang banyak menggunakan blok iterator. LINQ pada dasarnya malas di mana ia bisa - dan meletakkan segala sesuatu dalam daftar tidak berfungsi seperti itu.

Jon Skeet
sumber
1
Saya tidak yakin suka RandomSequenceatau tidak. Bagi saya, IEnumerable berarti -pertama dan terpenting- yang bisa saya ulangi dengan foreach, tetapi ini jelas akan menyebabkan loop yang tidak terbatas di sini. Saya akan menganggap ini sebagai penyalahgunaan konsep IEnumerable yang sangat berbahaya, tetapi YMMV.
Sebastian Negraszus
5
@SebastianNegraszus: Urutan angka acak tidak terbatas secara logis. Anda dapat dengan mudah membuat yang IEnumerable<BigInteger>mewakili urutan Fibonacci, misalnya. Anda dapat menggunakannya foreachdengan itu, tetapi tidak ada IEnumerable<T>jaminan bahwa itu akan terbatas.
Jon Skeet
42

Dengan kode "daftar", Anda harus memproses daftar lengkap sebelum dapat meneruskannya ke langkah berikutnya. Versi "hasil" langsung melewati item yang diproses ke langkah berikutnya. Jika "langkah selanjutnya" berisi ". Ambil (10)" maka versi "hasil" hanya akan memproses 10 item pertama dan melupakan sisanya. Kode "daftar" akan memproses semuanya.

Ini berarti bahwa Anda melihat perbedaan paling besar ketika Anda perlu melakukan banyak pemrosesan dan / atau memiliki daftar panjang item untuk diproses.

Hans Ke st ing
sumber
23

Anda dapat menggunakan yielduntuk mengembalikan item yang tidak ada dalam daftar. Berikut adalah sedikit contoh yang dapat diulang tanpa batas hingga daftar dibatalkan.

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);
    }
}

Ini menulis

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

... dll. ke konsol sampai dibatalkan.

Jason Whitted
sumber
10
object jamesItem = null;
foreach(var item in FilteredList())
{
   if (item.Name == "James")
   {
       jamesItem = item;
       break;
   }
}
return jamesItem;

Ketika kode di atas digunakan untuk mengulang melalui FilteredList () dan dengan asumsi item.Name == "James" akan dipenuhi pada item ke-2 dalam daftar, metode yang menggunakan yieldakan menghasilkan dua kali. Ini adalah perilaku malas.

Sedangkan metode menggunakan daftar akan menambahkan semua objek ke daftar dan meneruskan daftar lengkap ke metode panggilan.

Ini persis kasus penggunaan di mana perbedaan antara IEnumerable dan IList dapat disorot.

humblelistener
sumber
7

Contoh dunia nyata terbaik yang pernah saya lihat untuk penggunaan yieldadalah menghitung urutan Fibonacci.

Pertimbangkan kode berikut:

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;
        }
    }
}

Ini akan mengembalikan:

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

Ini bagus karena memungkinkan Anda untuk menghitung seri yang tak terbatas dengan cepat dan mudah, memberi Anda kemampuan untuk menggunakan ekstensi Linq dan hanya meminta apa yang Anda butuhkan.

Middas
sumber
5
Saya tidak bisa melihat apa pun "dunia nyata" dalam perhitungan urutan Fibonacci.
Nek
Saya setuju bahwa ini bukan benar-benar "dunia nyata," tetapi ide yang keren.
Casey
1

mengapa menggunakan [hasil]? Terlepas dari itu kode yang sedikit kurang, apa fungsinya bagi saya?

Terkadang bermanfaat, kadang tidak. Jika seluruh rangkaian data harus diperiksa dan dikembalikan maka tidak akan ada manfaat dalam menggunakan hasil karena semua yang dilakukannya hanyalah memperkenalkan overhead.

Ketika hasil benar-benar bersinar adalah ketika hanya sebagian parsial dikembalikan. Saya pikir contoh terbaik adalah menyortir. Asumsikan Anda memiliki daftar objek yang berisi tanggal dan jumlah dolar dari tahun ini dan Anda ingin melihat segelintir (5) catatan pertama tahun ini.

Untuk mencapai ini, daftar harus disortir naik berdasarkan tanggal, dan kemudian diambil 5 yang pertama. Jika ini dilakukan tanpa hasil, seluruh daftar harus disortir, hingga memastikan dua tanggal terakhir sudah beres.

Namun, dengan hasil, setelah 5 item pertama telah ditetapkan penyortiran berhenti dan hasilnya tersedia. Ini dapat menghemat banyak waktu.

Travis J
sumber
0

Pernyataan pengembalian hasil memungkinkan Anda untuk mengembalikan hanya satu item pada suatu waktu. Anda mengumpulkan semua item dalam daftar dan kembali mengembalikan daftar itu, yang merupakan overhead memori.

Prabhavith
sumber