await foreach terasa sederhana, tetapi di baliknya bekerja sepasang interface dengan kontrak yang jelas. IAsyncEnumerable<T> mewakili urutan asinkron dan membuka satu enumerator lewat method GetAsyncEnumerator(). Enumerator itu, bertipe IAsyncEnumerator<T>, menyediakan MoveNextAsync() untuk meminta item berikutnya, properti Current untuk membacanya, dan DisposeAsync() untuk membersihkan resource. Bab ini menelusuri interface tersebut secara manual agar mekanisme await foreach terlihat jelas.

Anatomi Dua Interface #

  • IAsyncEnumerable<T>: hanya punya satu method, GetAsyncEnumerator(CancellationToken), yang mengembalikan enumerator untuk satu kali penelusuran.
  • IAsyncEnumerator<T>: punya MoveNextAsync() yang mengembalikan ValueTask<bool> (true berarti ada item), properti Current bertipe T, dan DisposeAsync() bertipe ValueTask.

Perbandingan ini menegaskan posisinya di samping sepasang sinkronnya:

Pasangan sinkronPasangan asinkronLangkah ambil item
IEnumerable<T>IAsyncEnumerable<T>GetAsyncEnumerator()
IEnumerator<T>IAsyncEnumerator<T>MoveNext() vs MoveNextAsync()

Contoh: Menelusuri Antrean Notifikasi Secara Manual #

Kode berikut mengambil enumerator, menelusurinya dengan loop while, lalu membiarkan await using membersihkannya:

csharp
static async IAsyncEnumerable<string> AmbilNotifikasiAsync()
{
    await Task.Delay(30);
    yield return "Pesanan baru INV-201";
    await Task.Delay(30);
    yield return "Stok Kopi Gayo menipis";
}

// Pemanggil:
IAsyncEnumerable<string> antrean = AmbilNotifikasiAsync();

await using IAsyncEnumerator<string> enumerator = antrean.GetAsyncEnumerator();

while (await enumerator.MoveNextAsync())
{
    Console.WriteLine($"Notifikasi masuk: {enumerator.Current}");
}
Console.WriteLine("Antrean notifikasi habis");

Output yang Dihasilkan:

text
Notifikasi masuk: Pesanan baru INV-201
Notifikasi masuk: Stok Kopi Gayo menipis
Antrean notifikasi habis

Mari kita bedah baris-barisnya:

  1. antrean.GetAsyncEnumerator(): pemanggilan method produsen baru membuat urutan; enumerator diambil dari urutan itu dan belum ada item yang dikirim.
  2. await using ... enumerator: deklarasi await using memastikan DisposeAsync() dipanggil saat blok selesai; padanan asinkron dari using.
  3. await enumerator.MoveNextAsync(): setiap pemanggilan menunggu item berikutnya; hasil true berarti satu item siap di Current.
  4. enumerator.Current: properti menyimpan item terakhir yang berhasil diambil; pembacaannya hanya sah setelah MoveNextAsync mengembalikan true.
  5. Keluar loop: MoveNextAsync mengembalikan false ketika produsen habis, dan await using menjalankan pembersihan.

Ketahuan cara mengonsumsi urutan asinkron. Bab berikutnya berfokus pada sisi produsen: menulis method yang mengirim item asinkron dengan yield return.