Aracılığıyla paylaş


Yineleyiciler

Yazdığınız hemen her program bir koleksiyon üzerinden geçiş yapma ihtiyacı duyacaktır. 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 gibi kapsayıcılar üzerinde dolaşan bir nesnedir. Yineleyiciler şu için kullanılabilir:

  • Koleksiyondaki her öğe üzerinde eylem gerçekleştirme.
  • Özel bir koleksiyonun elemanlarını listeleme.
  • 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 tek ihtiyacınız olan foreach 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ı zaman uyumsuz olarak tüketmek için await foreach deyimini kullanabilirsiniz.

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

Bir sıra bir System.Collections.Generic.IEnumerable<T> olduğunda foreach kullanırsınız. Bir sıra bir System.Collections.Generic.IAsyncEnumerable<T> olduğunda await foreach kullanırsınız. İ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, yield return ve yield return ifadelerinin ayrı ayrı kullanıldığını göstererek bir yineleyici yönteminde birden fazla ayrık ifade kullanabileceğiniz gerçeğini vurgular. 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++;
}

Önceki örneklerin hepsinin eş zamanlı olmayan 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şturduğunu hayal edin. Veriler için bir his elde etmek için her N. veri öğesini örnekleyen bir yöntem yazabilirsiniz. Bu küçük yineleyici yöntem işe yarar.

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 return ifadesi hem de yield return ifadesi 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 yield return kullanma veya özgün yöntemi birden fazla yönteme ayırarak bazı yöntemlerde return ve diğerlerinde yield return kullanma seçeneğiniz vardır.

Son yöntemi her yerde yield return kullanmanıza izin vermek için yönteminizi 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. Birincisi return kullanan ve ikincisi yield return kullanan. Boolean ifadesine göre boş bir koleksiyon veya ilk beş tek sayıyı 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 oluşturmak için yield return deyimini kullanır.

Daha derinlemesine bir bakış foreach

foreach deyimi, IEnumerable<T> ve IEnumerator<T> arabirimlerini kullanarak bir koleksiyonun tüm öğeleri üzerinde yinelemek için standart bir deyime genişletilir. 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 üretilen tam kod daha karmaşıktır ve GetEnumerator() tarafından döndürülen nesne IDisposable arabirimini uyguladığı 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.
    }
}

Sayacın veya sıralayıcının yönetimi, enumerator türünün özelliklerine bağlıdır. Genel eşzamanlı durumda, finally yan tümcesi şu şekilde genişletilir:

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

Genel zaman uyumsuz durum şu şekildedir:

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

Ancak, enumerator türü korumalı bir türse ve enumerator türünden IDisposable veya IAsyncDisposable türüne örtük bir dönüştürme yoksa, finally ifadesi boş bir bloğa genişletilir:

finally
{
}

enumerator'den IDisposable türüne özellikle bir dönüştürme varsa ve enumerator boş değer atanamayan bir değer türüyse, yan tümce finally şu şekilde genişletilir:

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

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