paint-brush
IEnumerable Düşündüğünüz Gibi Değil ve Kodunuzu Bozuyorile@dmitriislabko
Yeni tarih

IEnumerable Düşündüğünüz Gibi Değil ve Kodunuzu Bozuyor

ile Dmitrii Slabko14m2025/02/18
Read on Terminal Reader

Çok uzun; Okumak

IEnumerable ile ilgili en yaygın hata olan tekrarlı sayım konusunu detaylıca inceleyelim; ancak bu sefer biraz daha derinlere inip tekrarlı sayımların neden bir hata olduğunu ve yakalanması ve yeniden üretilmesi zor hatalar da dahil olmak üzere hangi potansiyel sorunlara yol açabileceğini inceleyeceğiz.
featured image - IEnumerable Düşündüğünüz Gibi Değil ve Kodunuzu Bozuyor
Dmitrii Slabko HackerNoon profile picture
0-item

ÖZET: IEnumerable ile ilgili en yaygın hatayı -tekrarlanan numaralandırma- ayrıntılı olarak inceleyelim; ancak bu sefer biraz daha derinlere inip tekrarlanan numaralandırmanın neden bir hata olduğunu ve yakalanması ve yeniden üretilmesi zor hatalar da dahil olmak üzere hangi potansiyel sorunlara yol açabileceğini inceleyeceğiz.

IEnumerable nedir?

İlk önce ilk şeyler - IEnumerable'ın hem genel hem de genel olmayan ne olduğunu gözden geçirelim (bu konuda oldukça fazla makale olduğu için bir kez daha). Birçok geliştirici, birçok röportaj ve kod incelemesinin gösterdiği gibi, IEnumerable örneklerini farkında olmadan koleksiyonlar olarak görür ve biz de buradan başlayacağız.


IEnumerable'ın arayüz tanımına baktığımızda şunu görüyoruz:

 public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }


Numaratörlerin ve benzerlerinin ayrıntılarına girmeyeceğiz; çok önemli bir şeyi belirtmek yeterli: IEnumerable bir koleksiyon değildir . Çoğu koleksiyon türü IEnumerable'ı uygular, ancak bu tüm IEnumerable uygulamalarını koleksiyonlara dönüştürmez. Şaşırtıcı bir şekilde, birçok geliştiricinin IEnumerable'ı tüketen veya üreten kod uygularken gözden kaçırdığı şey budur ve bu, büyük bir sorun potansiyeline sahiptir.


Peki, IEnumerable nedir? IEnumerable için birçok farklı uygulama vardır, ancak basitlik adına bunları tek bir (oldukça belirsiz) tanımda özetleyebiliriz: yinelemede öğeler üreten bir kod parçasıdır. Bellek içi koleksiyonlar için bu kod, geçerli öğeyi alttaki koleksiyondan okur ve varsa dahili işaretçisini bir sonraki öğeye taşır. Daha karmaşık durumlar için mantık çok çeşitli olabilir ve paylaşılan durumu değiştirmeyi veya paylaşılan duruma bağlı olmayı da içerebilen her türlü yan etkiye sahip olabilir.


Artık IEnumerable'ın ne olduğu konusunda biraz daha iyi bir fikrimiz var ve bu da bize, bu noktalarda herhangi bir varsayımda bulunmayacak şekilde, tüketen kodu uygulamamız gerektiğini gösteriyor:

  • Bir ürünü üretmek için gereken maliyet - yani, bir ürünün bir tür depolama alanından alınması (yeniden kullanılması) veya yaratılması durumunda;
  • aynı ürün sonraki yinelemelerde tekrar üretilebilir;
  • sonraki yinelemeleri etkileyebilecek (veya etkilemeyecek) olası yan etkiler.


Gördüğümüz gibi, bu, örneğin bellek içi koleksiyonlar üzerinde yineleme yaparken genel kuralların neredeyse tam tersidir:

  • bir koleksiyon yineleme sırasında değiştirilemez - eğer bir koleksiyon değiştirilirse, bu koleksiyondaki bir sonraki öğeye geçildiğinde bir istisnaya neden olur;
  • Aynı koleksiyon (aynı öğeleri içeren) üzerinde yineleme yapmak her zaman aynı sonuçları üretecek ve her zaman aynı maliyetlere sahip olacaktır.


IEnumerable'a bakmanın güvenli bir yolu onu 'talep üzerine veri üreticisi' olarak algılamaktır. Bu veri üreticisinin verdiği tek garanti, çağrıldığında başka bir öğe tedarik edeceği veya daha fazla öğenin mevcut olmadığını bildireceğidir. Diğer her şey belirli bir veri üreticisinin uygulama ayrıntılarıdır. Bu arada, burada bir IEnumerable örneği üzerinde yineleme yapmayı sağlayan IEnumerator arayüzünün sözleşmesini açıkladık.


İsteğe bağlı veri üreticisinin bir diğer önemli parçası, yineleme başına bir öğe üretmesi ve tüketen kodun, üreticinin üretebildiği her şeyi tüketmek veya tüketimi daha erken durdurmak isteyip istemediğine karar verebilmesidir. İsteğe bağlı veri üreticisi herhangi bir potansiyel 'gelecek' öğesi üzerinde çalışmayı bile denemediğinden, bu, tüketim erken bittiğinde kaynakları kurtarmaya olanak tanır.


Yani, IEnumerable üreticilerini uygularken, tüketim kalıpları hakkında hiçbir varsayımda bulunmamalıyız. Tüketiciler, tüketimi herhangi bir noktada başlatabilir ve durdurabilir.

Tekrarlanan yinelemelerin potansiyel etkileri.

Şimdi, IEnumerable'ı tüketmenin doğru yolunu tanımladığımıza göre, tekrarlanan yinelemelerin birkaç örneğini ve bunların potansiyel etkilerini inceleyelim.


Olumsuz örneklere geçmeden önce, IEnumerable'ın bellek içi bir koleksiyonu -dizi, liste, karma kümesi, vb.- taklit ettiğinde, tekrarlanan yinelemelerin başlı başına bir zararı olmadığını belirtmekte fayda var. Çoğu durumda, bellek içi koleksiyonlar üzerinden IEnumerable'ı tüketen kod, eşleşen koleksiyon türlerini tüketen kod kadar (neredeyse) verimli bir şekilde çalışır. Elbette, Linq'in örneğin bellek içi koleksiyonlar için vektörleştirilmiş CPU talimatlarını kullanmasına veya karmaşık Linq ifadeleri için birden fazla arayüz yöntemi çağrısını tek bir çağrıda sıkıştırmasına izin verecek birçok önemli performans artışı gördüğü için, belirli durumlarda farklılıklar olabilir, ancak mutlaka olumsuz olmayabilir. Daha fazla ayrıntı için lütfen şu makaleleri okuyun: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#linq ve https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/#linq


Ancak, kod kalitesi açısından bakıldığında, IEnumerable üzerinde birden fazla yineleme yapmak kötü bir uygulama olarak kabul edilir, çünkü perde arkasında hangi somut uygulamanın geleceğinden asla emin olamayız.

Yan not: IEnumerable bir arayüz olduğundan, somut tipler yerine kullanılması derleyicinin sanal metot çağrıları ('callvirt' IL talimatı) yayınlamasını zorlar, somut bir alt sınıf bu metodu sanal olmayan olarak uygulasa bile, dolayısıyla sanal olmayan bir metot çağrısı yeterli olur. Sanal metot çağrıları daha pahalıdır, çünkü metot adresini çözmek için her zaman örnek metot tablosundan geçmeleri gerekir; ayrıca, olası metot satır içi yerleştirmeyi engellerler. Bu bir mikro-optimizasyon olarak düşünülebilirken, arayüzler yerine somut tipler kullanılsaydı farklı performans ölçümleri gösterecek oldukça fazla kod yolu vardır.

Tekrarlanan bir yineleme gerçekten kötü bir seçim olduğunda.

Küçük bir uyarı: Bu örnek, anonimleştirilmiş ve tüm gerçek uygulama ayrıntılarının soyutlandığı gerçek hayattan bir kod parçasına dayanmaktadır.

Bu kod parçası, gelen parametre listesi için uzak bir uç noktadan veri alıyordu.

 async Task<IEnumerable<IData>> RetrieveAndProcessDataAsync(IList<int> ids, CancellationToken ct) { var retrievalTasks = ids.Select(id => externalService.QueryForDataAsync(id, ct)); await Task.WhenAll(retrievalTasks); return retrievalTasks.Select(t => t.Result); }

Burada ne yanlış gidebilir? En basit örneği inceleyelim:

 var results = await RetrieveAndProcessDataAsync(ids, cancellationToken); var output = results.ToArray();


Birçok geliştirici bu kodu güvenli olarak değerlendirirdi - çünkü yöntem çıktısını bellek içi bir koleksiyona dönüştürerek tekrarlanan yinelemeleri önler. Ama öyle mi?


Detaylara girmeden önce bir test çalışması yapalım. Test için çok basit bir 'externalService' uygulamasını ele alabiliriz:

 record Data(int Value); class Service { private static int counter = 0; public async Task<IData> QueryForDataAsync(int id, CancellationToken ct) { var timestamp = Stopwatch.GetTimestamp(); await Task.Delay(TimeSpan.FromMilliseconds(30), ct); int cv = Interlocked.Increment(ref counter); Console.WriteLine($"QueryForData - id={id} - {cv}; took {Stopwatch.GetElapsedTime(timestamp).TotalMilliseconds:F0} ms"); return new Data(id); } }


Daha sonra testi çalıştırabiliriz:

 var externalService = new Service(); var results = (await RetrieveAndProcessDataAsync([1, 2, 3], CancellationToken.None)).ToList(); Console.WriteLine("Querying completed"); int count = results.Count(); if (count == 0) { Console.WriteLine("No results"); } else { var array = results.ToArray(); Console.WriteLine($"Retrieved {array.Length} elements"); } Console.WriteLine($"Getting the count again: {results.Count()}");


Ve çıktıyı alalım:

 QueryForData - id=3 - 1; took 41 ms QueryForData - id=1 - 3; took 43 ms QueryForData - id=2 - 2; took 42 ms QueryForData - id=1 - 4; took 33 ms QueryForData - id=2 - 5; took 30 ms QueryForData - id=3 - 6; took 31 ms Querying completed Retrieved 3 elements Getting the count again: 3


Burada bir şeyler ters gidiyor, değil mi? Giriş argümanında yalnızca 3 kimliğimiz olduğundan 'QueryForData' çıktısını yalnızca 3 kez almayı beklerdik. Ancak çıktı, ToList() çağrısı tamamlanmadan önce bile yürütme sayısının iki katına çıktığını açıkça gösteriyor.


Bunun nedenini anlamak için RetrieveAndProcessDataAsync metoduna bakalım:

 1: var retrievalTasks = ids.Select(id => externalService.QueryForDataAsync(id, ct)); 2: await Task.WhenAll(retrievalTasks); 3: return retrievalTasks.Select(t => t.Result);


Ve şu çağrıya bir bakalım:

 (await RetrieveAndProcessDataAsync([1, 2, 3], CancellationToken.None)).ToList();


RetrieveAndProcessDataAsync metodu çağrıldığında aşağıdaki olaylar gerçekleşir.


1. satırda bir IEnumerable<Task<Data>> örneği alıyoruz - bizim durumumuzda, 3 elemanlı bir girdi dizisi gönderdiğimiz için 3 görev olurdu. Her görev, iş parçacığı havuzu tarafından yürütülmek üzere sıraya alınır ve bir iş parçacığı kullanılabilir olur olmaz başlar. Bu görevler için kesin tamamlanma noktası, iş parçacığı havuzu zamanlama özellikleri ve bu kodun çalışacağı somut donanım nedeniyle belirsizdir.


2. satırda Task.WhenAll çağrısı IEnumerable<Task<Data>> örneğinden gelen tüm görevlerin tamamlandığından emin olur; esasen, bu noktada QueryForDataAsync yönteminden ilk 3 çıktıyı alırız. 2. satır tamamlandığında, 3 görevin de tamamlandığından emin olabiliriz.


Ancak 3. satır tüm şeytanların pusu kurduğu yerdir. Hadi onları ortaya çıkaralım.


'retrievalTasks' değişkeni (1. satırda) bir IEnumerable<Task<Data>> örneğidir. Şimdi bir adım geri gidelim ve IEnumerable'ın bir üreticiden başka bir şey olmadığını hatırlayalım - belirli bir türün örneklerini üreten (oluşturan veya yeniden kullanan) bir kod parçası. Bu durumda 'retrievalTasks' değişkeni şunları yapacak bir kod parçasıdır:


  • 'ids' koleksiyonunu gözden geçir;
  • Bu koleksiyonun her bir elemanı için externalService.QueryForDataAsync metodunu çağırırdı;
  • önceki çağrı tarafından üretilen bir Görev örneğini döndür.


IEnumerable<Task<Data>> örneğimizin ardındaki tüm bu mantığı biraz farklı bir şekilde ifade edebiliriz. Lütfen bu kod parçasının orijinal ids.Select(id => externalService.QueryForDataAsync(id, ct)) ifadesinden oldukça farklı görünse de, tam olarak aynı şeyi yaptığını unutmayın .


 IEnumerable<Task<Data>> DataProducer(IList<int> ids, CancellationToken ct) { foreach (int id in ids) { var task = externalService.QueryForData(id, ct); yield return task; } }


Yani, 'retrievalTasks' değişkenini sabit önceden tanımlanmış girdi kümesine sahip bir fonksiyon çağrısı olarak ele alabiliriz. Bu fonksiyon, değişken değerini her çözdüğümüzde çağrılacaktır. RetrieveAndProcessDataAsync yöntemini bu fikri tam olarak yansıtacak ve ilk uygulamaya kesinlikle eşit şekilde çalışacak şekilde yeniden yazabiliriz:


 async Task<IEnumerable<Data>> RetrieveAndProcessDataAsync(IList<int> ids, CancellationToken ct) { var retrievalFunc = () => DataProducer(ids, ct); await Task.WhenAll(retrievalFunc()); return retrievalFunc().Select(t => t.Result); }


Şimdi test kod çıktımızın neden iki katına çıkarıldığını çok açık bir şekilde görebiliyoruz: 'retrievalFunc' fonksiyonu iki kez çağrılıyor... Tüketen kodumuz aynı IEnumerable örneği üzerinde çalışmaya devam ederse, bu, her yinelemede mantığını tekrar tekrar çalıştıracak olan 'DataProducer' metoduna yapılan tekrarlanan çağrılara eşit olacaktır.


Umarım şimdi IEnumerable'ın tekrarlanan yinelemelerinin ardındaki mantık anlaşılmıştır.

Tekrarlanan yinelemelerin olası diğer etkileri.

Yine de bu kod örneğiyle ilgili söylenmesi gereken bir şey var.


Yeniden yazılan uygulamaya tekrar bakalım:

 IEnumerable<Task<Data>> DataProducer(IList<int> ids, CancellationToken ct) { foreach (int id in ids) { var task = externalService.QueryForData(id, ct); yield return task; } } async Task<IEnumerable<Data>> RetrieveAndProcessDataAsync(IList<int> ids, CancellationToken ct) { var retrievalFunc = () => DataProducer(ids, ct); await Task.WhenAll(retrievalFunc()); // First producer call. return retrievalFunc().Select(t => t.Result); // Second producer call. }


Bu durumda üretici her seferinde yeni görev örnekleri oluşturur ve biz bunu iki kez çağırırız. Bu, Task.WhenAll ve .Select(t => t.Result) çağırdığımızda bu iki kod parçasının üzerinde çalıştığı görev örneklerinin farklı olduğu gibi oldukça tuhaf ve o kadar da belirgin olmayan bir gerçeğe yol açar. Beklenen (ve böylece tamamlanan) görevler, yöntemin sonuçlarını döndürdüğü görevlerle aynı değildir.


Yani, burada üretici iki farklı görev kümesi oluşturur. İlk görev kümesi eşzamansız olarak beklenir - Task.WhenAll çağrısı - ancak ikinci görev kümesi beklenmez. Bunun yerine, kod doğrudan Result özelliği alıcısına çağrı yapar ve bu da aslında kötü şöhretli sync-over-async anti-desendir. Bu anti-desenin ayrıntılarına girmeyeceğim çünkü bu geniş bir konu. Stephen Toub'un bu makalesi bu konu hakkında epeyce ışık tutuyor: https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/


Ancak, eksiksiz olması adına, bu kodun neden olabileceği bazı potansiyel sorunlar şunlardır:

  • Masaüstünde (WinForms, WPF, MAUI) veya .Net Fx ASP.NET uygulamalarında kullanıldığında çıkmazlar;
  • Yüksek yükler altında iş parçacığı havuzu açlığı.


Bu basit görevleri üreten mevcut kod örneğinden soyutlarsak, tekrarlanan yinelemelerin herhangi bir işlem için kolayca birden fazla yürütmeye neden olabileceği ve idempotent olmayabileceği (yani, aynı girdilerle yapılan sonraki çağrıların farklı sonuçlar üretmesi veya hatta basitçe başarısız olması kaçınılmazdır) gerçeğiyle karşı karşıya kalırız. Örneğin, hesap bakiyesi değişir.


Bu işlemler idempotent olsa bile, yüksek hesaplama maliyetlerine sahip olabilirler ve bu nedenle tekrarlanan yürütmeleri kaynaklarımızı boşuna tüketir. Ve bulutta çalışan koddan bahsediyorsak, bu kaynakların ödememiz gereken bir maliyeti olabilir.


Yine, IEnumerable örnekleri üzerinde tekrarlanan yinelemeler gözden kaçırılması oldukça kolay olduğundan, bir uygulamanın neden çöktüğünü, neden çok fazla kaynak harcadığını (para da dahil) veya neden yapmaması gereken şeyleri yaptığını bulmak çok zor olabilir.

Biraz renk katalım.

Orijinal test kodunu alıp biraz değiştirelim:

 var externalService = new Service(); var cts = new CancellationTokenSource(); // New line. var results = (await RetrieveAndProcessDataAsync([1, 2, 3], cts.Token)); // Using cts.Token instead of a default token, and not materializing the IEnumerable. Console.WriteLine("Querying completed"); int count = results.Count(); if (count == 0) { Console.WriteLine("No results"); } else { var array = results.ToArray(); Console.WriteLine($"Retrieved {array.Length} elements"); } cts.Cancel(); // New line. Console.WriteLine($"Getting the count again: {results.Count()}");


Bu kodu çalıştırmayı okuyucuya bırakacağım. Tekrarlanan yinelemelerin beklenmedik şekilde karşılaşabileceği olası yan etkilerin iyi bir gösterimi olacak.

Bu kodu nasıl düzeltebilirim?

Bir bakalım:

 async Task<IEnumerable<IData>> RetrieveAndProcessDataAsync(IList<int> ids, CancellationToken ct) { var retrievalTasks = ids.Select(id => externalService.QueryForDataAsync(id, ct)).ToArray(); // Adding .ToArray() call. await Task.WhenAll(retrievalTasks); return retrievalTasks.Select(t => t.Result); }


Başlangıçtaki IEnumerable<Task<Data>> ya tek bir .ToArray() çağrısı ekleyerek IEnumerable örneğini bellek içi bir koleksiyona 'somutlaştırırız' ve bellek içi koleksiyon üzerindeki sonraki tüm yinelemeler tam olarak varsaydığımız şeyi yapar - tekrarlanan kod yürütmelerinin neden olduğu beklenmedik yan etkiler olmadan verileri bellekten okur.


Temel olarak, geliştiriciler bu tür bir kod yazdığında (ilk kod örneğinde olduğu gibi), normalde bu verinin 'taşa kazınmış' olduğunu ve erişildiğinde beklenmedik hiçbir şeyin olmayacağını varsayarlar. Ancak, az önce gördüğümüz gibi, bu gerçeklerden oldukça uzaktır.


Yöntemi daha da geliştirebiliriz ama bunu bir sonraki bölüme bırakalım.

IEnumerable üretimi hakkında.

IEnumerable'ın yanlış anlaşılmalara dayalı kullanımından kaynaklanabilecek sorunlara baktık; IEnumerable'ı kullanırken bu varsayımlardan hiçbirinin dikkate alınmaması gerektiğini belirttik:


  • Bir ürünü üretmek için gereken maliyet - yani, bir ürünün bir tür depolama alanından alınması (yeniden kullanılması) veya yaratılması durumunda;
  • aynı ürünün sonraki yinelemelerde tekrar üretilebilmesi;
  • sonraki yinelemeleri etkileyebilecek (veya etkilemeyecek) olası yan etkiler.


Şimdi, IEnumerable üreticilerinin tüketicilerine (ideal olarak) yerine getirmesi gereken vaade bir bakalım:

  • ürünler 'talep üzerine' üretilir - 'önceden' hiçbir çaba gösterilmesi beklenmez;
  • Tüketiciler istedikleri zaman yinelemeyi durdurma özgürlüğüne sahiptir ve bu, tüketimin devam etmesi halinde ihtiyaç duyulacak kaynakları korumalıdır ;
  • Eğer yineleme (tüketim) başlamamışsa hiçbir kaynak kullanılmamalıdır.


Yine önceki kod örneğimizi bu açıdan inceleyelim.

 async Task<IEnumerable<IData>> RetrieveAndProcessDataAsync(IList<int> ids, CancellationToken ct) { var retrievalTasks = ids.Select(id => externalService.QueryForDataAsync(id, ct)).ToArray(); await Task.WhenAll(retrievalTasks); return retrievalTasks.Select(t => t.Result); }


Esasen, bu kod bu vaatleri yerine getirmez, çünkü tüm zor kaldırma IEnumerable'ı üretmeye başlamadan önce ilk iki satırda yapılır. Yani, herhangi bir tüketici tüketimi daha erken durdurmaya karar verirse veya hiç başlatmazsa, QueryForDataAsync yöntemi yine de tüm girdiler için çağrılır.


İlk iki satırın davranışını göz önünde bulundurarak, yöntemi bellek içi bir koleksiyon üretecek şekilde yeniden yazmak çok daha iyi olacaktır, örneğin:

 async Task<IList<IData>> RetrieveAndProcessDataAsync(IList<int> ids, CancellationToken ct) { var retrievalTasks = ids.Select(id => externalService.QueryForDataAsync(id, ct)).ToArray(); await Task.WhenAll(retrievalTasks); return retrievalTasks.Select(t => t.Result).ToArray(); }


Bu uygulama herhangi bir 'talep üzerine' garanti sağlamaz - aksine, verilen girdiyi işlemek için gereken tüm işin tamamlanacağı ve eşleşen sonuçların döndürüleceği çok açıktır.


Ancak, 'talep üzerine veri üreticisi' davranışına ihtiyacımız varsa, bunu sağlamak için yöntemin tamamen yeniden yazılması gerekir. Örneğin:

 async IAsyncEnumerable<Data> RetrieveAndProcessDataAsAsyncEnumerable(IList<int> ids, [EnumeratorCancellation] CancellationToken ct) { foreach (int id in ids) { var result = await externalService.QueryForData(id, ct); yield return result; } }


Geliştiriciler genellikle IEnumerable'ın bu sözleşme özelliklerini düşünmese de, onu kullanan diğer kodlar sıklıkla bu özelliklerle eşleşen varsayımlarda bulunur. Bu nedenle, IEnumerable'ı üreten kod bu özelliklerle eşleştiğinde, tüm uygulama daha iyi çalışır.

Çözüm.

Umarım bu makale okuyucunun bir koleksiyon sözleşmesi ile IEnumerable sözleşmesinin özellikleri arasındaki farkı görmesine yardımcı olmuştur. Koleksiyonlar genellikle öğeleri için bir miktar depolama alanı (genellikle bellekte) ve depolanan öğelerin üzerinden geçme yolları sağlar; salt okunur olmayan koleksiyonlar da depolanan öğeleri değiştirmeye/eklemeye/kaldırmaya izin vererek bu sözleşmeyi genişletir. Koleksiyonlar depolanan öğeler konusunda oldukça tutarlı olsa da, IEnumerable esasen bu konuda çok yüksek oynaklık beyan eder çünkü öğeler bir IEnumerable örneği üzerinde yineleme yapıldığında üretilir.


Peki, IEnumerable'a gelindiğinde en iyi uygulamalar neler olurdu? Sadece madde listesini verelim:

  • Tekrarlanan yinelemelerden her zaman kaçının - eğer gerçekten bunu amaçlamıyorsanız ve sonuçlarını anlamıyorsanız. Bir IEnumerable örneğine birden fazla Linq uzantı yöntemi (örneğin .Where ve .Select ) zincirlemek güvenlidir ancak gerçek bir yinelemeye neden olacak diğer tüm çağrılardan kaçınılmalıdır. İşleme mantığı bir IEnumerable üzerinde birden fazla geçiş gerektiriyorsa, ya onu bellek içi bir koleksiyona dönüştürün ya da mantığın öğe başına tek bir geçişe değiştirilip değiştirilemeyeceğini inceleyin.
  • Bir IEnumerable üretirken asenkron kod içeriyorsa, onu IAsyncEnumerable olarak değiştirmeyi veya IEnumerable'ı 'somutlaştırılmış' bir gösterimle değiştirmeyi düşünün - örneğin, paralel yürütmenin avantajlarından yararlanmak ve tüm görevler tamamlandıktan sonra sonuçları döndürmek istediğinizde.
  • IEnumerable üreten kod, yinelemenin erken durması veya hiç başlamaması durumunda kaynak harcamaktan kaçınılmasını sağlayacak şekilde derlenmelidir.
  • Veri türleri için IEnumerable'ı, ayrıntılarına ihtiyacınız olmadığı sürece kullanmayın. Kodunuzun bir miktar 'genelleştirme'ye ihtiyacı varsa, IList veya IReadOnlyCollection gibi 'talep üzerine veri üreticisi' davranışını ima etmeyen diğer koleksiyon türü arayüzlerini tercih edin.