Iterator

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 item 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.

Iterasi dengan foreach

Menghitung koleksi sederhana: Kata foreach kunci menghitung koleksi, menjalankan pernyataan yang disematkan sekali untuk setiap elemen dalam koleksi:

foreach (var item in collection)
{
    Console.WriteLine(item?.ToString());
}

Itu saja. Untuk melakukan iterasi atas semua konten koleksi, foreach pernyataan adalah semua yang Anda butuhkan. Pernyataan itu foreach bukan sihir, meskipun. Ini bergantung pada dua antarmuka generik yang ditentukan dalam pustaka core .NET untuk menghasilkan kode yang diperlukan untuk mengulangi koleksi: IEnumerable<T> dan IEnumerator<T>. Mekanisme ini dijelaskan 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 rekan asinkron. Dalam setiap kasus, Anda akan mengganti jenis IEnumerable<T> pengembalian 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 melakukan trik:

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 tidak menjadi 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. Salah satu yang menggunakan return, dan detik yang 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 ini:

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++;
    }
}

Lihatlah 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 ini foreach meluas ke idiom standar yang menggunakan IEnumerable<T> antarmuka dan IEnumerator<T> untuk melakukan iterasi di semua elemen koleksi. Ini juga meminimalkan kesalahan yang dilakukan pengembang dengan tidak mengelola sumber daya dengan benar.

Kompilator menerjemahkan perulangan yang foreach ditunjukkan dalam contoh pertama menjadi sesuatu yang mirip dengan konstruksi 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.
    }
}

Kompilator 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 di mana enumerator dibuang 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 adalah jenis enumerator yang disegel dan tidak ada konversi implisit dari jenis enumerator ke IDisposable atau IAsyncDisposable, finally klausa meluas ke blok kosong:

finally
{
}

Jika ada konversi implisit dari jenis enumerator ke, dan IDisposable merupakan jenis nilai yang tidak dapat diubah ke null, enumerator klausul akan diperluas finallyke:

finally
{
   ((IDisposable)enumerator).Dispose();
}

Untungnya, Anda tidak perlu mengingat semua detail ini. Pernyataan ini foreach menangani semua nuansa untuk Anda. Pengkompilasi akan menghasilkan kode yang benar untuk salah satu konstruksi ini.