Kata yield
kunci memungkinkan Anda untuk membuat IEnumerable<T>
formulir di blok iterator . Blok iterator ini mendukung eksekusi yang ditangguhkan dan jika Anda tidak terbiasa dengan konsep itu mungkin tampak hampir ajaib. Namun, pada akhirnya hanya kode yang dijalankan tanpa trik aneh.
Blok iterator dapat digambarkan sebagai gula sintaksis di mana kompiler menghasilkan mesin negara yang melacak sejauh mana enumerasi enumerable telah berkembang. Untuk menghitung enumerable, Anda sering menggunakan foreach
loop. Namun, satu foreach
lingkaran juga merupakan gula sintaksis. Jadi, Anda adalah dua abstraksi yang dihapus dari kode asli yang mengapa awalnya mungkin sulit untuk memahami bagaimana semuanya bekerja bersama.
Asumsikan bahwa Anda memiliki blok iterator yang sangat sederhana:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
Blok iterator nyata sering memiliki kondisi dan loop tetapi ketika Anda memeriksa kondisi dan membuka gulungannya, mereka masih berakhir sebagai yield
pernyataan yang disatukan dengan kode lain.
Untuk menghitung blok iterator, sebuah foreach
loop digunakan:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Berikut ini hasilnya (tidak ada kejutan di sini):
Mulai
1
Setelah 1
2
Setelah 2
42
Akhir
Sebagaimana dinyatakan di atas foreach
adalah gula sintaksis:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Dalam upaya untuk mengurai ini saya telah membuat diagram urutan dengan abstraksi dihapus:
Mesin negara yang dihasilkan oleh kompiler juga mengimplementasikan enumerator tetapi untuk membuat diagram lebih jelas, saya telah menunjukkannya sebagai instance yang terpisah. (Ketika mesin negara disebutkan dari utas lain, Anda benar-benar mendapatkan instance terpisah tetapi detail itu tidak penting di sini.)
Setiap kali Anda menyebut blok iterator Anda, instance baru dari mesin state dibuat. Namun, tidak ada kode Anda di blok iterator yang dieksekusi sampai enumerator.MoveNext()
dijalankan untuk pertama kalinya. Inilah cara kerja eksekusi yang ditangguhkan. Berikut adalah contoh (agak konyol):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
Pada titik ini iterator belum dieksekusi. The Where
klausul menciptakan baru IEnumerable<T>
yang membungkus yang IEnumerable<T>
dikembalikan oleh IteratorBlock
tetapi enumerable ini belum disebutkan. Ini terjadi ketika Anda menjalankan foreach
loop:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Jika Anda menghitung enumerable dua kali maka instance baru dari mesin state dibuat setiap kali dan blok iterator Anda akan mengeksekusi kode yang sama dua kali.
Perhatikan bahwa metode LINQ suka ToList()
, ToArray()
, First()
, Count()
dll akan menggunakan foreach
loop untuk menghitung enumerable tersebut. Misalnya ToList()
akan menghitung semua elemen yang dapat dihitung dan menyimpannya dalam daftar. Anda sekarang dapat mengakses daftar untuk mendapatkan semua elemen enumerable tanpa blok iterator mengeksekusi lagi. Ada trade-off antara menggunakan CPU untuk menghasilkan elemen-elemen enumerable berulang kali dan memori untuk menyimpan elemen-elemen enumerasi untuk mengaksesnya berkali-kali saat menggunakan metode seperti ToList()
.