Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Hampir setiap program yang Anda tulis akan memiliki beberapa kebutuhan untuk melakukan iterasi atas koleksi. Anda akan menulis kode yang memeriksa setiap item dalam koleksi.
Anda juga akan membuat metode iterator, yang merupakan metode yang menghasilkan iterator untuk elemen kelas tersebut. Iterator adalah objek yang melintasi kontainer, terutama daftar. Iterator dapat digunakan untuk:
- Melakukan tindakan pada setiap elemen dalam koleksi.
- Menghitung koleksi kustom.
- Memperluas LINQ atau pustaka lainnya.
- Membuat alur data tempat data mengalir secara efisien melalui metode iterator.
Bahasa C# menyediakan fitur untuk menghasilkan dan mengkonsumsi urutan. Urutan ini dapat diproduksi dan dikonsumsi secara sinkron atau asinkron. Artikel ini memberikan gambaran umum tentang fitur-fitur tersebut.
Menggunakan foreach dalam iterasi
Enumerasi koleksi itu sederhana: Kata kunci foreach melakukan enumerasi terhadap koleksi, mengeksekusi pernyataan tertanam sekali untuk setiap elemen dalam koleksi.
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
Itu saja. Untuk melakukan iterasi atas semua elemen dalam koleksi, perintah foreach adalah semua yang Anda butuhkan. Pernyataan foreach itu bukan sihir, meskipun. Ini bergantung pada dua antarmuka generik yang ditentukan dalam pustaka inti .NET untuk menghasilkan kode yang diperlukan untuk mengulangi koleksi: IEnumerable<T> dan IEnumerator<T>. Mekanisme ini dijelaskan secara lebih rinci di bawah ini.
Kedua antarmuka ini juga memiliki rekan non-generik: IEnumerable dan IEnumerator. Versi generik lebih disukai untuk kode modern.
Ketika urutan dihasilkan secara asinkron, Anda dapat menggunakan await foreach pernyataan untuk mengonsumsi urutan secara asinkron:
await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}
Ketika urutannya adalah System.Collections.Generic.IEnumerable<T>, Anda menggunakan foreach. Ketika urutannya adalah System.Collections.Generic.IAsyncEnumerable<T>, Anda menggunakan await foreach. Dalam kasus terakhir, urutan dihasilkan secara asinkron.
Sumber enumerasi dengan metode iterator
Fitur hebat lain dari bahasa C# memungkinkan Anda membangun metode yang membuat sumber untuk enumerasi. Metode ini disebut sebagai metode iterator. Metode iterator menentukan cara menghasilkan objek secara berurutan saat diminta. Anda menggunakan yield return kata kunci kontekstual untuk menentukan metode iterator.
Anda dapat menulis metode ini untuk menghasilkan urutan bilangan bulat dari 0 hingga 9:
public IEnumerable<int> GetSingleDigitNumbers()
{
yield return 0;
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
yield return 6;
yield return 7;
yield return 8;
yield return 9;
}
Kode di atas menunjukkan pernyataan yang berbeda yield return untuk menyoroti fakta bahwa Anda dapat menggunakan beberapa pernyataan diskrit yield return dalam metode iterator. Anda dapat (dan sering melakukan) menggunakan konstruksi bahasa lain untuk menyederhanakan kode metode iterator. Definisi metode di bawah ini menghasilkan urutan angka yang sama persis:
public IEnumerable<int> GetSingleDigitNumbersLoop()
{
int index = 0;
while (index < 10)
yield return index++;
}
Anda tidak perlu memutuskan satu atau yang lain. Anda dapat memiliki pernyataan sebanyak yield return yang diperlukan untuk memenuhi kebutuhan metode Anda:
public IEnumerable<int> GetSetsOfNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
index = 100;
while (index < 110)
yield return index++;
}
Semua contoh sebelumnya ini akan memiliki mitra asinkron. Dalam setiap kasus, Anda akan mengganti pengembalian jenis IEnumerable<T> dengan IAsyncEnumerable<T>. Misalnya, contoh sebelumnya akan memiliki versi asinkron berikut:
public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
int index = 0;
while (index < 10)
yield return index++;
await Task.Delay(500);
yield return 50;
await Task.Delay(500);
index = 100;
while (index < 110)
yield return index++;
}
Itulah sintaks untuk iterator sinkron dan asinkron. Mari kita pertimbangkan contoh dunia nyata. Bayangkan Anda berada di proyek IoT dan sensor perangkat menghasilkan aliran data yang sangat besar. Untuk merasakan data, Anda mungkin menulis metode yang mengambil sampel setiap elemen data Nth. Metode iterator kecil ini berhasil menyelesaikan tugasnya.
public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
int index = 0;
foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}
Jika membaca dari perangkat IoT menghasilkan urutan asinkron, Anda akan memodifikasi metode seperti yang ditunjukkan metode berikut:
public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
int index = 0;
await foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}
Ada satu batasan penting pada metode iterator: Anda tidak dapat memiliki return pernyataan dan yield return pernyataan dalam metode yang sama. Kode berikut tidak akan dikompilasi:
public IEnumerable<int> GetSingleDigitNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
// generates a compile time error:
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
return items;
}
Pembatasan ini biasanya bukan masalah. Anda memiliki pilihan untuk menggunakan yield return di seluruh metode, atau memisahkan metode asli menjadi beberapa metode, beberapa menggunakan return, dan beberapa menggunakan yield return.
Anda dapat memodifikasi metode terakhir sedikit untuk digunakan yield return di mana-mana:
public IEnumerable<int> GetFirstDecile()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
foreach (var item in items)
yield return item;
}
Terkadang, jawaban yang tepat adalah membagi metode iterator menjadi dua metode yang berbeda. Yang menggunakan return, dan yang kedua menggunakan yield return. Pertimbangkan situasi di mana Anda mungkin ingin mengembalikan koleksi kosong, atau lima angka ganjil pertama, berdasarkan argumen boolean. Anda dapat menulisnya sebagai dua metode berikut:
public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
if (getCollection == false)
return new int[0];
else
return IteratorMethod();
}
private IEnumerable<int> IteratorMethod()
{
int index = 0;
while (index < 10)
{
if (index % 2 == 1)
yield return index;
index++;
}
}
Lihat metode di atas. Yang pertama menggunakan pernyataan standar return untuk mengembalikan koleksi kosong, atau iterator yang dibuat oleh metode kedua. Metode kedua menggunakan yield return pernyataan untuk membuat urutan yang diminta.
Mendalami lebih dalam foreach
Pernyataan foreach ini berkembang menjadi idiom standar yang menggunakan antarmuka IEnumerable<T> dan IEnumerator<T> untuk mengiterasi semua elemen dalam koleksi. Ini juga meminimalkan kesalahan yang dilakukan pengembang dengan tidak mengelola sumber daya dengan benar.
Pengompilasi menerjemahkan foreach loop yang terlihat dalam contoh pertama menjadi sesuatu yang mirip dengan struktur ini.
IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
Kode yang tepat yang dihasilkan oleh pengkompilasi lebih rumit, dan menangani situasi di mana objek yang dikembalikan oleh GetEnumerator() mengimplementasikan IDisposable antarmuka. Ekspansi penuh menghasilkan kode lebih seperti ini:
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}
Kompiler menerjemahkan sampel asinkron pertama menjadi sesuatu yang mirip dengan konstruksi ini:
{
var enumerator = collection.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of async enumerator.
}
}
Cara pengelolaan enumerator tergantung pada karakteristik jenis enumerator. Dalam kasus sinkron umum, finally klausul diperluas ke:
finally
{
(enumerator as IDisposable)?.Dispose();
}
Kasus asinkron umum diperluas ke:
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
Namun, jika jenis enumerator adalah tipe yang tertutup dan tidak ada konversi implisit dari jenis enumerator ke IDisposable atau IAsyncDisposable, maka klausa finally akan meluas menjadi blok kosong.
finally
{
}
Jika ada konversi implisit dari jenis enumerator ke IDisposable, dan enumerator adalah jenis nilai yang bukan null, maka klausul finally akan diperluas menjadi:
finally
{
((IDisposable)enumerator).Dispose();
}
Untungnya, Anda tidak perlu mengingat semua detail ini. Pernyataan tersebut foreach menangani semua nuansa untuk Anda. Pengkompilasi akan menghasilkan kode yang benar untuk salah satu konstruksi ini.