Yineleyiciler

Yazdığınız hemen her programın bir koleksiyon üzerinde yinelemesi gerekir. Koleksiyondaki her öğeyi inceleyen bir kod yazacaksınız.

Ayrıca, bu sınıfın öğeleri için yineleyici oluşturan yöntemler olan yineleyici yöntemleri de oluşturacaksınız. Yineleyici, özellikle listeler olmak üzere kapsayıcıdan geçen bir nesnedir. Yineleyiciler şu için kullanılabilir:

  • Koleksiyondaki her öğe üzerinde eylem gerçekleştirme.
  • Özel koleksiyon numaralandırılıyor.
  • LINQ veya diğer kitaplıkları genişletme.
  • Verilerin yineleyici yöntemleri aracılığıyla verimli bir şekilde aktığı bir veri işlem hattı oluşturma.

C# dili hem oluşturma hem de kullanma dizileri için özellikler sağlar. Bu diziler zaman uyumlu veya zaman uyumsuz olarak üretilebilir ve kullanılabilir. Bu makalede bu özelliklere genel bir bakış sağlanır.

foreach ile yineleme

Bir koleksiyonu listelemek basittir: foreach anahtar sözcüğü bir koleksiyonu numaralandırır ve koleksiyondaki her öğe için katıştırılmış deyimi bir kez yürütür:

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

Hepsi bu kadar. Bir koleksiyonun tüm içeriğini yinelemek için ihtiyacınız foreach olan tek şey deyimidir. Ama foreach ifade sihirli değil. Bir koleksiyonu yinelemek için gereken kodu oluşturmak için .NET core kitaplığında tanımlanan iki genel arabirime dayanır: IEnumerable<T> ve IEnumerator<T>. Bu mekanizma aşağıda daha ayrıntılı olarak açıklanmıştır.

Bu arabirimlerin her ikisi de genel olmayan karşılıklara sahiptir: IEnumerable ve IEnumerator. Genel sürümler modern kod için tercih edilir.

Bir sıra zaman uyumsuz olarak oluşturulduğunda, sırayı await foreach zaman uyumsuz olarak kullanmak için deyimini kullanabilirsiniz:

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

Bir sıra bir System.Collections.Generic.IEnumerable<T>olduğunda kullanırsınız foreach. Bir sıra bir System.Collections.Generic.IAsyncEnumerable<T>olduğunda kullanırsınız await foreach. İkinci durumda, sıra zaman uyumsuz olarak oluşturulur.

Yineleyici yöntemleriyle numaralandırma kaynakları

C# dilinin bir diğer harika özelliği de numaralandırma için kaynak oluşturan yöntemler oluşturmanıza olanak tanır. Bu yöntemler yineleyici yöntemler olarak adlandırılır. Yineleyici yöntemi, istendiğinde bir dizideki nesnelerin nasıl oluşturulacağı tanımlar. Yineleyici yöntemi tanımlamak için bağlamsal anahtar sözcükleri kullanırsınız yield return .

0 ile 9 arasında tamsayı dizisini oluşturmak için bu yöntemi yazabilirsiniz:

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

Yukarıdaki kod, bir yineleyici yönteminde birden çok ayrık yield return deyim kullanabileceğiniz gerçeğini vurgulamak için ayrı yield return deyimleri gösterir. Yineleyici yönteminin kodunu basitleştirmek için diğer dil yapılarını kullanabilirsiniz (ve genellikle yapabilirsiniz). Aşağıdaki yöntem tanımı tam olarak aynı sayı dizisini oluşturur:

public IEnumerable<int> GetSingleDigitNumbersLoop()
{
    int index = 0;
    while (index < 10)
        yield return index++;
}

Birini veya diğerini seçmek zorunda değilsin. Yönteminizin gereksinimlerini karşılamak için gereken sayıda yield return deyiminiz olabilir:

public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

Yukarıdaki örneklerin tümünün zaman uyumsuz bir karşılığı olacaktır. Her durumda dönüş türünü IEnumerable<T> ile IAsyncEnumerable<T>değiştirirsiniz. Örneğin, önceki örnekte aşağıdaki zaman uyumsuz sürüm bulunur:

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

Bu, hem zaman uyumlu hem de zaman uyumsuz yineleyiciler için söz dizimidir. Şimdi gerçek bir dünya örneğini ele alalım. Bir IoT projesinde olduğunuzu ve cihaz algılayıcılarının çok büyük bir veri akışı oluşturup oluşturmadığınızı düşünün. Veriler için bir his elde etmek için her N. veri öğesini örnekleyen bir yöntem yazabilirsiniz. Bu küçük yineleyici yöntemi şu hileyi yapar:

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

IoT cihazından okuma zaman uyumsuz bir dizi oluşturursa, aşağıdaki yöntemin gösterdiği gibi yöntemini değiştirirsiniz:

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

Yineleyici yöntemlerinde önemli bir kısıtlama vardır: aynı yöntemde hem deyim returnyield return hem de deyim olamaz. Aşağıdaki kod derlenmez:

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

Bu kısıtlama normalde bir sorun değildir. Yöntemin tamamında kullanma yield return veya özgün yöntemi birden çok yönteme ayırma seçeneğiniz vardır; bazıları ve kullananlarreturnyield return.

Son yöntemi her yerde kullanmak yield return üzere biraz değiştirebilirsiniz:

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

Bazen doğru yanıt, yineleyici yöntemini iki farklı yönteme bölmektir. returnkullanan bir ve kullanan bir saniyeyield return. Boole bağımsız değişkenine göre boş bir koleksiyon veya ilk beş tek sayı döndürmek isteyebileceğiniz bir durum düşünün. Bunu şu iki yöntem olarak yazabilirsiniz:

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

Yukarıdaki yöntemlere bakın. birincisi, boş bir koleksiyon veya ikinci yöntem tarafından oluşturulan yineleyiciyi döndürmek için standart return deyimini kullanır. İkinci yöntem, istenen diziyi yield return oluşturmak için deyimini kullanır.

Daha ayrıntılı bilgi foreach

deyimi, foreach bir koleksiyonun tüm öğeleri arasında yinelemek için ve IEnumerator<T> arabirimlerini kullanan IEnumerable<T> standart bir deyime genişletir. Ayrıca, geliştiricilerin kaynakları düzgün yönetmeyerek yaptığı hataları da en aza indirir.

Derleyici, ilk örnekte gösterilen döngüleri foreach şu yapıya benzer bir şeye çevirir:

IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

Derleyici tarafından oluşturulan tam kod daha karmaşıktır ve tarafından GetEnumerator() döndürülen nesnenin arabirimini uyguladığı IDisposable durumları işler. Tam genişletme aşağıdakine benzer bir kod oluşturur:

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of enumerator.
    }
}

Derleyici, ilk zaman uyumsuz örneği şu yapıya benzer bir şeye çevirir:

{
    var enumerator = collection.GetAsyncEnumerator();
    try
    {
        while (await enumerator.MoveNextAsync())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of async enumerator.
    }
}

Numaralandırıcının atılma şekli, türünün enumeratorözelliklerine bağlıdır. Genel zaman uyumlu durumda yan tümcesi finally şu şekilde genişletir:

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

Genel zaman uyumsuz durum şu şekilde genişletiliyor:

finally
{
    if (enumerator is IAsyncDisposable asyncDisposable)
        await asyncDisposable.DisposeAsync();
}

Ancak türü korumalı bir türse enumerator ve veya türünden IAsyncDisposableenumeratorIDisposablefinally örtük dönüştürme yoksa yan tümcesi boş bir bloğa genişletilir:

finally
{
}

türünden enumeratorIDisposableöğesine örtük bir dönüştürme varsa ve enumerator boş değer atanamayan bir değer türündeyse, yan tümcesi finally şu şekilde genişletilir:

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

Neyse ki tüm bu ayrıntıları hatırlamanıza gerek yok. Deyimi foreach tüm bu nüansları sizin için işler. Derleyici, bu yapılardan herhangi biri için doğru kodu oluşturur.