yield return menyelesaikan sebagian besar kebutuhan iterator, tetapi ada situasi ketika kita perlu mengendalikan enumerator secara penuh: saat menulis struktur data sendiri, saat posisi berikutnya dihitung dari rumus, atau saat enumerator perlu status yang tidak bisa ditulis sebagai alur method. Pada bab ini kita membangun koleksi DeretGenap lengkap dengan enumerator manual, sehingga semua bagian kontrak iterasi dari bab IEnumerable dan IEnumerator terisi oleh kode kita sendiri.

Member yang Wajib Diisi #

Enumerator manual berarti mengisi dua interface sekaligus:

  • IEnumerable<T> pada koleksi: method GetEnumerator() dan versi non-generiknya.
  • IEnumerator<T> pada enumerator: Current bertipe T, MoveNext(), Reset(), Dispose(), serta IEnumerator.Current yang mengembalikan object.

Implementasi non-generik biasanya didelegasikan: IEnumerator.Current cukup mengembalikan Current yang bertipe T, dan IEnumerator.GetEnumerator() mengembalikan GetEnumerator() yang bertipe kuat. Method Reset() mengembalikan posisi ke keadaan awal; kontrak interface tidak mengatur perilakunya lebih ketat, sehingga perancang koleksi bebas menentukan apakah reset berjalan atau ditolak.

Koleksi DeretGenap #

Koleksi berikut tidak menyimpan angka sama sekali; enumerator menghitung angka genap berikutnya saat dibutuhkan:

csharp
public class DeretGenap : IEnumerable<int>
{
    private readonly int _batas;

    public DeretGenap(int batas)
    {
        _batas = batas;
    }

    public IEnumerator<int> GetEnumerator()
    {
        return new GenapEnumerator(_batas);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private class GenapEnumerator : IEnumerator<int>
    {
        private readonly int _batas;
        private int _nilai = 0;

        public GenapEnumerator(int batas)
        {
            _batas = batas;
        }

        public int Current => _nilai;

        object IEnumerator.Current => Current;

        public bool MoveNext()
        {
            _nilai += 2;
            return _nilai <= _batas;
        }

        public void Reset()
        {
            _nilai = 0;
        }

        public void Dispose()
        {
        }
    }
}

foreach (int angka in new DeretGenap(10))
{
    Console.WriteLine(angka);
}

Output yang Dihasilkan:

text
2
4
6
8
10

Mari kita bedah bagian pentingnya:

  1. GetEnumerator() mengembalikan new GenapEnumerator(_batas): setiap pemanggilan membuat enumerator baru dengan posisi sendiri, itulah sebabnya koleksi yang sama bisa diiterasi berkali-kali.
  2. object IEnumerator.Current => Current;: implementasi eksplisit antarmuka non-generik; kode konsumen generik tetap menerima int tanpa cast.
  3. _nilai += 2 di MoveNext(): angka berikutnya dihitung, bukan dibaca dari penyimpanan; deret mulai dari 2 karena _nilai awalnya 0.
  4. return _nilai <= _batas: enumerator berhenti setelah angka 10; pada percobaan berikutnya MoveNext() mengembalikan false dan foreach selesai.
  5. Dispose() kosong: enumerator ini tidak memegang sumber daya, sehingga pembersihan tidak diperlukan; bila enumerator membuka file atau koneksi, method inilah tempat menutupnya.

Koleksi, iterator, dan initializer kini lengkap di sisi konsumsi. Bab berikutnya beralih ke sisi pembuatan: bagaimana koleksi diisi secara ringkas sejak baris pertama.