Setiap loop foreach dan setiap query LINQ bekerja lewat satu interface kecil: IEnumerable<T>. Memahaminya akan menjelaskan kenapa LINQ bersifat lazy, kenapa sebagian urutan hanya bisa dibaca sekali, dan bagaimana yield return membuatmu bisa menulis urutanmu sendiri dengan loop biasa.
Apa itu IEnumerable
IEnumerable<T> punya satu method, GetEnumerator(). Enumerator yang dikembalikannya punya MoveNext() (maju, lalu memberi tahu apakah ada elemen) dan Current (elemennya). Sebuah foreach adalah cara singkat untuk menjalankan enumerator itu:
Output:
red green blue
red green blue
Enumerator dimulai sebelum elemen pertama, jadi MoveNext() yang pertama memindahkannya ke elemen itu. Saat MoveNext() mengembalikan false, urutannya sudah selesai. Blok using melakukan dispose pada enumerator, dan ini penting untuk iterator yang memegang resource seperti file yang sedang terbuka.
IEnumerable vs ICollection vs List
Interface koleksi membentuk sebuah tangga. Setiap anak tangga menjanjikan lebih banyak:
| Tipe | Menambahkan | Pemakaian umum |
|---|---|---|
IEnumerable<T> | hanya foreach | Parameter yang hanya kamu telusuri; hasil LINQ; urutan lazy |
IReadOnlyCollection<T> | Count | Nilai kembalian yang tidak boleh diubah pemanggil |
ICollection<T> | Count, Add, Remove, Contains | Kode yang menambah item ke koleksi jenis apa pun |
IReadOnlyList<T> | Akses lewat indeks [i] | Hasil read-only yang punya posisi |
IList<T> | Akses lewat indeks, Insert, RemoveAt | Kode yang mengubah isi berdasarkan posisi |
List<T> | Class konkret yang mengimplementasikan semuanya | Penyimpanan |
Method yang hanya melakukan perulangan pada input-nya sebaiknya menerima IEnumerable<T>, supaya pemanggil bisa mengirim array, list, set atau query LINQ tanpa konversi. Method yang mengembalikan data biasanya sebaiknya mengembalikan sesuatu yang menyatakan "ini sudah ada di memori", seperti List<T> atau IReadOnlyList<T>, karena nilai kembalian berupa IEnumerable<T> bisa saja sebuah query yang bekerja ulang setiap kali dibaca.
Output:
30
15
4.0
30
Menulis iterator dengan yield return
Untuk menghasilkan urutanmu sendiri, tulis method yang mengembalikan IEnumerable<T> dan pakai yield return untuk setiap elemen. Compiler mengubah method itu menjadi class yang mengimplementasikan enumerator untukmu.
Output:
0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri
Tidak ada list sama sekali di EvenNumbers. Setiap yield return menyerahkan satu nilai ke pemanggil dan menjeda method, sambil menjaga variabel lokalnya (di sini i) tetap hidup sampai pemanggil meminta nilai berikutnya.
Lazy: perhatikan urutan eksekusinya
Karena method-nya dijeda, badan method dan loop milik pemanggil berjalan bergantian. Mencetak dari kedua sisi akan memperlihatkan selang-selingnya:
Output:
Called Numbers(), nothing printed yet
start
yielding 1
Loop got 1
resumed after 1
yielding 2
Loop got 2
resumed after 2
yielding 3
Loop got 3
resumed after 3
end
Memanggil Numbers() tidak mencetak "start": pemanggilan itu hanya membuat enumerable-nya. Badan method baru dimulai pada MoveNext() pertama di dalam foreach, berjalan sampai yield return pertama, lalu berhenti. Setiap langkah berikutnya dilanjutkan di baris setelah yield. Where dan Select milik LINQ bekerja dengan cara yang sama (runtime mengimplementasikannya dengan class enumerator yang ditulis manual supaya lebih cepat, tapi hasilnya identik), itulah sebabnya query LINQ ditunda eksekusinya.
yield break
yield break mengakhiri urutan lebih awal, versi iterator dari return:
Output:
Subject: Invoice
From: shop@example.com
Kamu tidak bisa mencampur return value; dan yield return dalam satu method. Begitu sebuah method berisi yield, method itu adalah iterator, dan yield break adalah satu-satunya cara untuk keluar lebih awal.
Urutan tak terbatas
Karena hanya elemen yang diminta yang benar-benar dihitung, sebuah iterator bisa menggambarkan urutan yang tidak ada ujungnya. Pemakainya yang menentukan berapa banyak yang diambil:
Output:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon
while (true) aman di sini karena Take dan First berhenti meminta setelah mendapatkan yang mereka butuhkan. Sebaliknya, memanggil ToList() atau Count() pada urutan tak terbatas tidak akan pernah selesai, begitu juga enumerasi OrderBy di atasnya, karena pengurutan butuh semua elemen terlebih dahulu.
Jebakan urutan lazy
Pemeriksaan argumen berjalan terlambat. Seluruh badan iterator ditunda, termasuk validasi di bagian atasnya. Solusinya adalah memecah method-nya: method biasa memeriksa argumen lalu mengembalikan hasil dari iterator private.
Output:
LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call
Setiap enumerasi menjalankan method-nya lagi. Iterator tidak menyimpan apa pun dalam cache. Melakukan perulangan dua kali pada Numbers() akan mencetak "start" dua kali, membaca ulang file dua kali, atau memanggil web service dua kali. IDE menandai hal ini sebagai "possible multiple enumeration of IEnumerable". Panggil ToList() sekali saat kamu butuh elemen-elemennya lebih dari sekali.
finally berjalan saat pemakainya berhenti. Kalau pemanggil keluar dari foreach lebih awal, enumerator di-dispose dan blok finally apa pun di iterator akan berjalan, dan dengan cara inilah iterator yang membaca file menutup file tersebut:
Output:
open sensor
10
20
30
close sensor
done
Ada beberapa aturan yang ditegakkan compiler pada iterator: yield return tidak boleh muncul di dalam blok try yang punya catch (try/finally tidak masalah), di dalam lambda atau anonymous method, atau di method yang punya parameter ref atau out.
Async stream
Untuk urutan yang elemennya datang secara asinkron (halaman dari sebuah API, baris dari database), C# 8 ke atas (dengan .NET Core 3.0 ke atas) menggabungkan iterator dengan await lewat IAsyncEnumerable<T>:
static async IAsyncEnumerable<string> FetchPages()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(100); // simulate a network call
yield return $"page {page}";
}
}
await foreach (string p in FetchPages())
{
Console.WriteLine(p);
}
Pemakainya menggunakan await foreach, dan setiap elemen di-await saat elemen itu dihasilkan.
Kesalahan yang sering terjadi
- Mengembalikan
IEnumerable<T>dari sebuah query lalu membacanya berkali-kali. Setiap pembacaan menjalankannya ulang; kembalikanList<T>sebagai gantinya. - Memvalidasi argumen di dalam iterator. Pemeriksaannya ditunda; pecah menjadi method pembungkus dan iterator.
- Memanggil
Count()atauToList()pada iterator tak terbatas. Itu tidak akan pernah selesai; pakaiTaketerlebih dahulu. - Mengharapkan parameter
IEnumerable<T>punyaCountatau[i]. PakaiCount()milik LINQ (yang mungkin melakukan enumerasi) atau terimaIReadOnlyList<T>.
Pertanyaan yang sering diajukan
Apa itu IEnumerable di C#?
IEnumerable<T> adalah interface untuk apa pun yang bisa ditelusuri dengan foreach. Interface ini punya satu method, GetEnumerator(), yang mengembalikan IEnumerator<T> dengan MoveNext() dan Current. Array, list, dictionary, set, query LINQ dan method iterator semuanya mengimplementasikannya.
Apa perbedaan IEnumerable dan List di C#?
IEnumerable<T> hanya menjanjikan bahwa kamu bisa melakukan iterasi; interface ini tidak punya property Count, indexer maupun Add, dan isinya bisa dihitung secara lazy setiap kali kamu melakukan perulangan. List<T> adalah koleksi konkret yang disimpan di memori dan punya semua itu. Terima IEnumerable<T> sebagai parameter saat kamu hanya melakukan perulangan, dan kembalikan List<T> atau IReadOnlyList<T> saat pemanggil perlu mengakses lewat indeks atau menghitung jumlahnya.
Apa yang dilakukan yield return di C#?
yield return value; di dalam method yang mengembalikan IEnumerable<T> menyerahkan satu elemen ke pemanggil dan menjeda method di titik itu. Saat pemanggil meminta elemen berikutnya, method itu dilanjutkan di baris berikutnya. Compiler menulis ulang method tersebut menjadi sebuah state machine, jadi kamu cukup menulis loop dan mendapatkan urutan yang lazy.
Apa itu yield break di C#?
yield break; mengakhiri urutan: foreach milik pemanggil selesai dengan normal setelah elemen-elemen yang sudah dihasilkan. Ini padanan return untuk iterator, dan dipakai untuk berhenti lebih awal, misalnya saat batas tercapai atau input-nya kosong.
Kenapa method iterator saya tidak melempar exception saat dipanggil?
Badan method iterator tidak berjalan sampai urutannya dienumerasi, jadi pemeriksaan argumen di bagian atasnya baru berjalan saat MoveNext pertama, bukan saat pemanggilan. Untuk memvalidasi langsung, taruh pemeriksaannya di method publik biasa yang kemudian memanggil method iterator private yang berisi pernyataan yield.