Menu

IEnumerable dan yield return C#: Penjelasan Urutan Lazy

IEnumerable<T> adalah interface di balik foreach dan LINQ, dan yield return adalah cara termudah untuk mengimplementasikannya. Pelajari hubungan IEnumerable dengan ICollection dan List, cara method iterator berjalan langkah demi langkah, yield break, urutan tak terbatas, dan jebakan lazy evaluation.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

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:

TipeMenambahkanPemakaian umum
IEnumerable<T>hanya foreachParameter yang hanya kamu telusuri; hasil LINQ; urutan lazy
IReadOnlyCollection<T>CountNilai kembalian yang tidak boleh diubah pemanggil
ICollection<T>Count, Add, Remove, ContainsKode 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, RemoveAtKode yang mengubah isi berdasarkan posisi
List<T>Class konkret yang mengimplementasikan semuanyaPenyimpanan

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

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; kembalikan List<T> sebagai gantinya.
  • Memvalidasi argumen di dalam iterator. Pemeriksaannya ditunda; pecah menjadi method pembungkus dan iterator.
  • Memanggil Count() atau ToList() pada iterator tak terbatas. Itu tidak akan pernah selesai; pakai Take terlebih dahulu.
  • Mengharapkan parameter IEnumerable<T> punya Count atau [i]. Pakai Count() milik LINQ (yang mungkin melakukan enumerasi) atau terima IReadOnlyList<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.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai