Her foreach döngüsü ve her LINQ sorgusu tek bir küçük interface üzerinden çalışır: IEnumerable<T>. Onu anlamak, LINQ'in neden tembel olduğunu, bazı dizilerin neden yalnızca bir kez okunabildiğini ve yield return'ün düz bir döngüyle kendi dizilerinizi yazmanızı nasıl sağladığını açıklar.
IEnumerable nedir
IEnumerable<T>'nin tek bir metodu vardır, GetEnumerator(). Döndürdüğü enumerator'ın MoveNext() (ilerle ve bir eleman olup olmadığını bildir) ve Current (eleman) üyeleri vardır. Bir foreach o enumerator'ı yönetmenin kısaltmasıdır:
Çıktı:
red green blue
red green blue
Enumerator ilk elemandan önce başlar, bu yüzden ilk MoveNext() onun üzerine geçer. MoveNext() false döndürdüğünde dizi biter. using bloğu enumerator'ı dispose eder; bu, açık dosyalar gibi kaynaklar tutan iterator'lar için önemlidir.
IEnumerable, ICollection ve List
Koleksiyon interface'leri bir merdiven oluşturur. Her basamak daha fazlasını vaat eder:
| Tip | Ekledikleri | Tipik kullanım |
|---|---|---|
IEnumerable<T> | Yalnızca foreach | Yalnızca üzerinde döngü kurduğunuz parametreler; LINQ sonuçları; tembel diziler |
IReadOnlyCollection<T> | Count | Çağıranın değiştirmemesi gereken dönüş değerleri |
ICollection<T> | Count, Add, Remove, Contains | Herhangi türde bir koleksiyona ekleme yapan kod |
IReadOnlyList<T> | İndeks erişimi [i] | Konumlu salt okunur sonuçlar |
IList<T> | İndeks erişimi, Insert, RemoveAt | Konuma göre düzenleyen kod |
List<T> | Bunların hepsini uygulayan somut sınıf | Depolama |
Girdisi üzerinde yalnızca döngü kuran bir metot IEnumerable<T> kabul etmelidir; böylece çağıranlar dönüştürme yapmadan bir dizi, bir liste, bir küme ya da bir LINQ sorgusu geçirebilir. Veri döndüren bir metot genellikle "bu zaten bellekte" diyen bir şey, örneğin List<T> ya da IReadOnlyList<T>, döndürmelidir, çünkü bir IEnumerable<T> dönüş değeri her okunduğunda iş yapan bir sorgu olabilir.
Çıktı:
30
15
4.0
30
yield return ile bir iterator yazmak
Kendi dizinizi üretmek için IEnumerable<T> döndüren bir metot yazın ve her eleman için yield return kullanın. Derleyici metodu sizin için enumerator'ı uygulayan bir sınıfa çevirir.
Çıktı:
0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri
EvenNumbers'ın hiçbir yerinde liste yoktur. Her yield return çağırana bir değer verir ve metodu duraklatır; çağıran bir sonraki değeri isteyene kadar yerel değişkenlerini (burada i) canlı tutar.
Tembellik: yürütme sırasını izleyin
Metot duraklatıldığı için gövdesi ve çağıranın döngüsü sırayla çalışır. İki taraftan da yazdırmak iç içe geçmeyi gösterir:
Çıktı:
Called Numbers(), nothing printed yet
start
yielding 1
Loop got 1
resumed after 1
yielding 2
Loop got 2
resumed after 2
yielding 3
Loop got 3
resumed after 3
end
Numbers()'ı çağırmak "start" yazdırmadı: yalnızca enumerable'ı oluşturdu. Gövde foreach içindeki ilk MoveNext()'te başladı, ilk yield return'e kadar çalıştı ve durdu. Sonraki her adım yield'den sonraki satırdan devam etti. LINQ'in Where ve Select'i de aynı şekilde davranır (çalışma zamanı onları hız için elle yazılmış enumerator sınıflarıyla uygular, ama etki aynıdır); LINQ sorgularının ertelenmiş olmasının nedeni budur.
yield break
yield break diziyi erken bitirir; return'ün iterator sürümüdür:
Çıktı:
Subject: Invoice
From: shop@example.com
Tek bir metotta return value; ile yield return'ü karıştıramazsınız. Bir metot yield içerdiği anda bir iterator'dır ve ondan erken çıkmanın tek yolu yield break'tir.
Sonsuz diziler
Yalnızca istenen elemanlar hesaplandığı için bir iterator sonu olmayan bir diziyi tanımlayabilir. Ne kadar alınacağına tüketici karar verir:
Çıktı:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon
while (true) burada güvenlidir, çünkü Take ve First ihtiyaç duyduklarını aldıktan sonra istemeyi bırakır. Öte yandan sonsuz bir dizide ToList() ya da Count() çağırmak asla dönmez; onun üzerindeki bir OrderBy'ı dolaşmak da dönmez, çünkü sıralama önce her elemana ihtiyaç duyar.
Tembel dizilerin tuzakları
Argüman kontrolleri geç çalışır. Bir iterator'ın, en baştaki doğrulama dahil tüm gövdesi ertelenir. Çözüm metodu bölmektir: normal bir metot argümanları kontrol eder ve private bir iterator'ın sonucunu döndürür.
Çıktı:
LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call
Her dolaşma metodu yeniden çalıştırır. Bir iterator hiçbir şeyi önbelleğe almaz. Numbers() üzerinde iki kez döngü kurmak "start"'ı iki kez yazdırır, dosyayı iki kez yeniden okur ya da web servisini iki kez çağırır. IDE'ler bunu "possible multiple enumeration of IEnumerable" olarak işaretler. Elemanlara birden fazla kez ihtiyacınız olduğunda bir kez ToList() çağırın.
finally tüketici durduğunda çalışır. Çağıran foreach'ten erken çıkarsa enumerator dispose edilir ve iterator'daki her finally bloğu çalışır; bir dosyayı okuyan bir iterator onu böyle kapatır:
Çıktı:
open sensor
10
20
30
close sensor
done
Derleyicinin iterator'lara uyguladığı birkaç kural: yield return, catch'i olan bir try bloğunun içinde (bir try/finally sorun değildir), bir lambda ya da anonim metodun içinde veya ref ya da out parametreli bir metotta görünemez.
Async stream'ler
Elemanları asenkron olarak gelen diziler için (bir API'den sayfalar, bir veritabanından satırlar), C# 8 ve sonrası (.NET Core 3.0 ya da sonrasıyla) iterator'ları IAsyncEnumerable<T> aracılığıyla await ile birleştirir:
static async IAsyncEnumerable<string> FetchPages()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(100); // simulate a network call
yield return $"page {page}";
}
}
await foreach (string p in FetchPages())
{
Console.WriteLine(p);
}
Tüketici await foreach kullanır ve her eleman üretildikçe await edilir.
Yaygın hatalar
- Bir sorgudan
IEnumerable<T>döndürüp onu birçok kez okumak. Her okuma onu yeniden çalıştırır; bunun yerine birList<T>döndürün. - Bir iterator'da argümanları doğrulamak. Kontrol ertelenir; bir sarmalayıcı ve bir iterator olarak bölün.
- Sonsuz bir iterator'da
Count()ya daToList()çağırmak. Asla bitmez; önceTakekullanın. - Bir
IEnumerable<T>parametresininCount'u ya da[i]'si olmasını beklemek. LINQ'inCount()'unu (dolaşabilir) kullanın ya daIReadOnlyList<T>kabul edin.
Sıkça Sorulan Sorular
C#'ta IEnumerable nedir?
IEnumerable<T>, foreach ile üzerinde döngü kurulabilen her şeyin interface'idir. Tek bir metodu vardır, GetEnumerator(); bu da MoveNext() ve Current'a sahip bir IEnumerator<T> döndürür. Diziler, listeler, dictionary'ler, kümeler, LINQ sorguları ve iterator metotlar hepsi onu uygular.
C#'ta IEnumerable ile List arasındaki fark nedir?
IEnumerable<T> yalnızca dolaşabileceğinizi vaat eder; Count property'si, indeksleyicisi ve Add'i yoktur ve her döngüde tembel olarak hesaplanabilir. List<T> ise bunların hepsine sahip, bellekte saklanan somut bir koleksiyondur. Yalnızca döngü kurduğunuz parametrelerde IEnumerable<T> kabul edin, çağıranların indeksleme ya da sayma yapması gerektiğinde bir List<T> ya da IReadOnlyList<T> döndürün.
C#'ta yield return ne yapar?
IEnumerable<T> döndüren bir metodun içindeki yield return value; çağırana bir eleman verir ve metodu o noktada duraklatır. Çağıran bir sonraki elemanı istediğinde metot bir sonraki satırdan devam eder. Derleyici metodu bir durum makinesine yeniden yazar; siz bir döngü yazarsınız ve tembel bir dizi elde edersiniz.
C#'ta yield break nedir?
yield break; diziyi bitirir: çağıranın foreach'i zaten üretilmiş elemanlardan sonra normal şekilde sona erer. return'ün iterator karşılığıdır ve erken durmak için kullanılır, örneğin bir sınıra ulaşıldığında ya da girdi boş olduğunda.
Iterator metodum çağırdığımda neden istisna fırlatmıyor?
Bir iterator metodunun gövdesi dizi dolaşılana kadar çalışmaz, bu yüzden en baştaki argüman kontrolleri çağrıda değil ilk MoveNext'te çalışır. Hemen doğrulamak için kontrolleri, yield ifadelerini içeren private bir iterator metodunu çağıran normal bir public metoda koyun.