Yineleyiciler
Yazdığınız hemen her programın bir koleksiyon üzerinde yinelemesi gerekir. Koleksiyondaki her öğeyi inceleyen 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 şunların için kullanılabilir:
- Koleksiyondaki her öğe üzerinde eylem gerçekleştirme.
- Özel bir koleksiyonu listeleme.
- LINQ veya diğer kitaplıkları genişletme.
- Verilerin yineleyici yöntemler 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 olan foreach
tek şey deyimidir. Ama foreach
deyimi büyü 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 ikisinin de genel olmayan karşılıkları vardır: 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 dizi bir System.Collections.Generic.IEnumerable<T>olduğunda kullanırsınız foreach
. Bir dizi 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 nesnelerin bir sırayla nasıl oluşturulacağı tanımlar. Yineleyici bir yöntem 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ı yield return
deyimi kullanabileceğinizi vurgulayan ayrı yield return
deyimleri gösterir. Yineleyici yönteminin kodunu basitleştirmek için diğer dil yapılarını kullanabilirsiniz (ve genellikle kullanabilirsiniz). 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ğilsiniz. 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 örneği 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. Verilere yönelik bir his elde etmek için her N. veri öğesini örnekleyen bir yöntem yazabilirsiniz. Bu küçük yineleyici yöntemi şu işi 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 deyimi return
yield return
hem de deyimi olamaz. Aşağıdaki kod derlenemez:
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 return
kullananlar yield 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öntemi iki farklı yönteme bölmektir. return
kullanan 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öntemle 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. İlki, 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 derine in foreach
deyimi, foreach
bir koleksiyonun tüm öğeleri arasında yineleme yapmak için ve IEnumerator<T>
arabirimlerini kullanan IEnumerable<T>
standart bir deyime genişletir. Ayrıca geliştiricilerin kaynakları düzgün bir şekilde yönetmeyerek yaptığı hataları da en aza indirir.
Derleyici, ilk örnekte gösterilen döngüsünü şu yapıya benzer bir şeye çevirir foreach
:
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şler:
finally
{
(enumerator as IDisposable)?.Dispose();
}
Genel zaman uyumsuz durum şu şekilde genişler:
finally
{
if (enumerator is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
}
Ancak, türü enumerator
korumalı bir türse ve veya türünden IAsyncDisposable
enumerator
IDisposable
finally
örtük dönüştürme yoksa yan tümcesi boş bir bloğa genişletilir:
finally
{
}
türünden enumerator
türüne IDisposable
örtük bir dönüştürme varsa ve enumerator
boş değer atanamayan bir değer türündeyse, finally
yan tümcesi şu şekilde genişletilir:
finally
{
((IDisposable)enumerator).Dispose();
}
Neyse ki, tüm bu ayrıntıları hatırlamanız gerekmez. 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.