Menu

C# IEnumerable ve yield return: Tembel Diziler

IEnumerable<T>, foreach ve LINQ'in arkasındaki interface'tir; yield return ise onu uygulamanın en kolay yoludur. IEnumerable'ın ICollection ve List ile ilişkisini, bir iterator metodunun adım adım nasıl çalıştığını, yield break'i, sonsuz dizileri ve tembel değerlendirmenin tuzaklarını öğrenin.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

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:

TipEkledikleriTipik kullanım
IEnumerable<T>Yalnızca foreachYalnı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, ContainsHerhangi türde bir koleksiyona ekleme yapan kod
IReadOnlyList<T>İndeks erişimi [i]Konumlu salt okunur sonuçlar
IList<T>İndeks erişimi, Insert, RemoveAtKonuma göre düzenleyen kod
List<T>Bunların hepsini uygulayan somut sınıfDepolama

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 bir List<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 da ToList() çağırmak. Asla bitmez; önce Take kullanın.
  • Bir IEnumerable<T> parametresinin Count'u ya da [i]'si olmasını beklemek. LINQ'in Count()'unu (dolaşabilir) kullanın ya da IReadOnlyList<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.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA