foreach vs someList.ForEach () {}


167

Bir koleksiyon üzerinde yinelemenin birçok yolu var. Farklılıklar olup olmadığını veya neden bir yolu diğerinin üzerinde kullandığınızı merak ediyorum.

İlk tür:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Diğer yol:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Başımın üstünden, yukarıda kullandığım anonim delege yerine, belirtebileceğiniz yeniden kullanılabilir bir delege olacağınızı varsayalım ...


Yanıtlar:


135

İkisi arasında önemli ve yararlı bir ayrım vardır.

.ForEach forkoleksiyonu yinelemek için bir döngü kullandığından , bu geçerlidir (edit: .net 4.5'ten önce - uygulama değişti ve her ikisi de atıyor):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

oysa foreachbir numaralandırıcı kullanır, bu nedenle bu geçerli değildir:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: Bu kodu uygulamanıza kopyalamayın!

Bu örnekler sadece arasındaki farkları göstermek için vardır, en iyi uygulama değildir ForEach()ve foreach.

forDöngü içindeki bir listeden öğe kaldırmanın yan etkileri olabilir. En yaygın olanı bu sorunun yorumlarında açıklanmaktadır.

Genellikle, bir listeden birden fazla öğeyi kaldırmak istiyorsanız, gerçek kaldırmadan hangi öğelerin kaldırılacağının belirlenmesini ayırmak istersiniz. Kodunuzu kompakt tutmaz, ancak hiçbir öğeyi kaçırmamanızı garanti eder.


35
o zaman bile, bunun yerine someList.RemoveAll (x => x.RemoveMe) kullanmalısınız
Mark Cidade

2
Linq ile her şey daha iyi yapılabilir. Sadece foreach içindeki koleksiyonu değiştirmeye bir örnek gösteriyordum ...

2
RemoveAll (), <T> Listesinde bir yöntemdir.
Mark Cidade

3
Muhtemelen bunun farkındasınız, ancak insanlar öğeleri bu şekilde çıkarmaya dikkat etmelidir; N öğesini kaldırırsanız, yineleme öğeyi (N + 1) atlar ve bunu temsilcinizde görmez veya kaldırma şansınız olmaz, tıpkı bunu döngü için kendi başınıza yapmışsınız gibi.
El Zorko

9
Listeyi bir for döngüsü ile geriye doğru yinelerseniz, dizin kaydırma sorunları olmadan öğeleri kaldırabilirsiniz.
S. Tarık Çetin

78

Burada bazı kodlarımız vardı (VS2005 ve C # 2.0'da), önceki mühendislerin kullanmak list.ForEach( delegate(item) { foo;});yerine kendi yollarından gittikleri yerlerdeforeach(item in list) {foo; }; yazdıkları tüm kodlar için çıktılar. örneğin bir dataReader'dan satır okumak için bir kod bloğu.

Bunu neden yaptığını hala tam olarak bilmiyorum.

Dezavantajları list.ForEach() :

  • C # 2.0'da daha ayrıntılı. Ancak, C # 3'ten sonra "=> " sözdizimini bazı hoş kısa ifadeler yapmak için .

  • Daha az tanıdık geliyor. Bu kodu korumak zorunda olan insanlar bunu neden böyle yaptığınızı merak edecektir. Belli bir neden olmadığına karar vermem biraz zaman aldı, belki de yazarın akıllı görünmesini sağlamak (kodun geri kalanının kalitesi bunu zayıflattı). Ayrıca })delege kod bloğunun sonunda " " ile daha az okunabilirdi .

  • Ayrıca bkz. Bill Wagner'in neden foreach'ların döngüler gibi veya döngüler gibi diğer döngülere neden tercih edildiğinden bahsettiği "Etkili C #: 50 C #'nizi Geliştirmenin Özel Yolları" adlı kitabı - ana nokta, derleyicinin oluşturmanın en iyi yoluna karar vermesine izin vermenizdir. döngü. Derleyicinin gelecekteki bir sürümü daha hızlı bir teknik kullanmayı başarırsa, kodunuzu değiştirmek yerine foreach ve rebuilding kullanarak bunu ücretsiz olarak alacaksınız.

  • Bir foreach(item in list)yapı kullanmak için izin verir breakveyacontinue yineleme veya döngüden çıkmanız gerekiyorsa. Ancak, bir foreach döngüsü içindeki listeyi değiştiremezsiniz.

Bunun list.ForEachbiraz daha hızlı olduğunu görünce şaşırdım . Ancak bu muhtemelen onu kullanmak için geçerli bir neden değildir, bu erken optimizasyon olacaktır. Uygulamanız döngü kontrolü değil, bir veritabanı veya web hizmeti kullanıyorsa, neredeyse her zaman zamanın olduğu yerde olacaktır. Ve siz forde bir döngü ile karşılaştırdınız mı? list.ForEach dahili olarak kullanılması nedeniyle daha hızlı olabilir forve sargısız bir döngü daha da hızlı olurdu.

Buna katılmıyorum list.ForEach(delegate) sürümü önemli bir şekilde "daha işlevsel" dir. Bir işlevi bir işleve geçirir, ancak sonuçta veya program organizasyonunda büyük bir fark yoktur.

Ben foreach(item in list)"tam olarak nasıl yapıldığını söylüyor" diye düşünmüyorum - afor(int 1 = 0; i < count; i++) döngü bunu yapar, bir foreachdöngü derleyici kadar kontrol seçim bırakır.

Yeni bir projede, foreach(item in list)ortak kullanıma ve okunabilirliğe uymak için çoğu döngüde kullanmak ve list.Foreach()C # 3 " =>" operatörü ile daha zarif veya kompakt bir şey yapabileceğinizde sadece kısa bloklar için kullanmak benim hissim . Bu gibi durumlarda, zaten daha spesifik olan bir LINQ genişletme yöntemi olabilir ForEach(). Bak bakalım Where(), Select(), Any(), All(), Max()diğer birçok LINQ yöntemlerinden veya biri zaten sen döngüden istediğini yapmaz.


Sadece merak için ... Microsoft uygulamasına bakın ... referenceource.microsoft.com/#mscorlib/system/collections/…
Alexandre

17

Eğlenmek için reflektör içine Liste attı ve bu ortaya çıkan C #:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

Benzer şekilde, foreach tarafından kullanılan numaralandırıcıdaki MoveNext şudur:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach, MoveNext'ten çok daha kesilmiş - çok daha az işleme - daha etkili bir şeye JIT olacak.

Buna ek olarak, foreach () ne olursa olsun yeni bir Numaralandırıcı tahsis edecektir. GC arkadaşınızdır, ancak aynı foreach'i tekrar tekrar yapıyorsanız, aynı delegeyi tekrar kullanmanın aksine daha fazla atılabilir nesne yapacak - AMA - bu gerçekten bir saçmalık durumudur. Tipik kullanımda çok az fark görür veya hiç fark etmez.


1
Foreach tarafından oluşturulan kodun derleyici sürümleri arasında aynı olacağının garantisi yoktur. Oluşturulan kod gelecekteki bir sürümle geliştirilebilir.
Anthony

1
.NET Core 3.1'den itibaren ForEach hala daha hızlı.
jackmott

13

Onları farklı kılan iki belirsiz şey biliyorum. Git beni!

İlk olarak, listedeki her öğe için bir temsilci yapma konusunda klasik bir hata var. Foreach anahtar sözcüğünü kullanırsanız, tüm delegeleriniz listenin son öğesine başvurabilir:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

List.ForEach yönteminde bu sorun yoktur. Yinelemenin geçerli öğesi, dış lambda'ya bir argüman olarak değere göre iletilir ve daha sonra iç lambda, bu argümanı kendi kapanışında doğru bir şekilde yakalar. Sorun çözüldü.

(Ne yazık ki, ForEach'ın bir uzantı yöntemi yerine bir liste üyesi olduğuna inanıyorum, ancak kendiniz tanımlamak kolaydır, ancak bu tesisi herhangi bir numaralandırılabilir türde bulabilirsiniz.)

İkinci olarak, ForEach yöntemi yaklaşımının bir sınırlaması vardır. Verim iadesi kullanarak IEnumerable uyguluyorsanız, lambda içinde verim iadesi yapamazsınız. Dolayısıyla, bir koleksiyondaki öğeler arasında döngüsel olarak bir şeyler vermek için bu yöntemle döngü yapmak mümkün değildir. Foreach anahtar sözcüğünü kullanmanız ve döngü içindeki geçerli döngü değerinin manuel olarak bir kopyasını oluşturarak kapatma sorununu gidermeniz gerekir.

Daha fazla burada


5
Bahsettiğiniz sorun foreachC # 5'te "düzeltildi". Stackoverflow.com/questions/8898925/…
StriplingWarrior

13

Sanırım someList.ForEach()çağrı kolayca paralelleştirilebilirken, normal foreachparalel çalıştırmak o kadar kolay değil. Farklı çekirdeklerde birkaç farklı delege kolayca yönetebilirsiniz, bu normal bir şeyle o kadar kolay değildir foreach.
Sadece 2 sentim


2
Sanırım çalışma zamanı motoru bunu otomatik olarak paralelleştirebilirdi. Aksi takdirde, hem foreach hem de .ForEach, her eylem temsilcisindeki havuzdan bir iplik kullanılarak elle paralelleştirilebilir
Isak Savo

@Isak ama bu yanlış bir varsayım olurdu. Anonim yöntem bir yerel veya üyeyi kaldırırsa, çalışma zamanı 8 kolay bir şekilde paralellik gösteremez
Rune FS

6

Dedikleri gibi, şeytan ayrıntılarda ...

İki toplama numaralandırma yöntemi arasındaki en büyük fark foreach, devleti taşıması, oysa ForEach(x => { })bunu taşımamasıdır.

Ancak biraz daha derinlemesine inceleyelim, çünkü kararınızı etkileyebilecek bazı şeyler var ve her iki durum için kod yazarken dikkat etmeniz gereken bazı uyarılar var.

List<T>Küçük deneyimizde davranışı gözlemlemek için kullanalım . Bu deneme için .NET 4.7.2 kullanıyorum:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

foreachİlk önce bunu tekrarlayalım :

foreach (var name in names)
{
    Console.WriteLine(name);
}

Bunu şu şekilde genişletebiliriz:

using (var enumerator = names.GetEnumerator())
{

}

Numaralandırıcı eldeyken, örtülerin altına baktığımızda:

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

İki şey hemen ortaya çıkıyor:

  1. Altta yatan koleksiyon hakkında derinlemesine bilgi sahibi olan durumsal bir nesne döndürülür.
  2. Koleksiyonun kopyası sığ bir kopyadır.

Bu elbette hiçbir şekilde güvenli değildir. Yukarıda belirtildiği gibi, yineleme sırasında koleksiyonun değiştirilmesi sadece kötü mojo'dur.

Peki, koleksiyonun yineleme sırasında koleksiyonun dışına çıkarak yineleme sırasında geçersiz hale gelmesi sorunu ne olacak? En iyi uygulamalar, koleksiyonun işlemler ve yineleme sırasında sürümlendirilmesini ve temel alınan koleksiyonun ne zaman değiştiğini algılamak için sürümleri kontrol etmenizi önerir.

Burada işler gerçekten bulanıklaşıyor. Microsoft belgelerine göre:

Koleksiyonda öğe ekleme, değiştirme veya silme gibi değişiklikler yapılırsa, numaralandırıcının davranışı tanımlanmamıştır.

Peki, bu ne anlama geliyor? Örnek vermek gerekirse, List<T>istisna işleme uygulamalarının uygulanması IList<T>, uygulayan tüm koleksiyonların aynı şeyi yapacağı anlamına gelmez . Bu Liskov İkame İlkesinin açık bir ihlali gibi görünüyor:

Bir üst sınıfın nesneleri, uygulamayı bozmadan alt sınıflarının nesneleri ile değiştirilebilir.

Başka bir sorun, numaralandırıcının uygulaması gerektiğidir IDisposable- bu, yalnızca arayan yanlış yaparsa değil, yazar Disposedeseni doğru bir şekilde uygulamıyorsa, başka bir potansiyel bellek sızıntısı kaynağı anlamına gelir .

Son olarak, bir ömür boyu sorunumuz var ... yineleyici geçerliyse, ancak temel alınan koleksiyon gittiğinde ne olur? Şimdi ne olduğunun fotoğrafı ... bir koleksiyonun ömrünü ve yineleyicilerini ayırdığınızda sorun istiyorsunuz.

Şimdi inceleyelim ForEach(x => { }):

names.ForEach(name =>
{

});

Bu, aşağıdakilere genişler:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Önemli not aşağıdakilerdir:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Bu kod hiçbir numaralandırıcı (hiçbir şey Dispose) ayırmaz ve yineleme sırasında duraklamaz .

Bunun, temel alınan koleksiyonun sığ bir kopyasını da gerçekleştirdiğini, ancak koleksiyonun artık bir anlık görüntü olduğunu unutmayın. Yazar, koleksiyonun değiştirilmesi veya eski hale gelmesi için doğru bir denetim uygulamazsa, anlık görüntü hala geçerlidir.

Bu sizi hiçbir şekilde yaşam boyu sorunlardan korumaz ... temeldeki koleksiyon kaybolursa, şimdi ne olduğunu gösteren sığ bir kopyanız var ... ama en azından Dispose probleminiz yok yetimsiz yineleyicilerle uğraşmak ...

Evet, yineleyiciler dedim ... bazen devlete sahip olmak avantajlıdır. Bir veritabanı imlecine benzer bir şey korumak istediğinizi varsayalım ... belki birden fazla foreachstil Iterator<T>'nin yolu budur. Kişisel olarak bu tasarım tarzından hoşlanmıyorum, çünkü çok fazla yaşam boyu sorun var ve güvendiğiniz koleksiyonların yazarlarının iyi ifadelerine güveniyorsunuz (kelimenin tam anlamıyla kendiniz sıfırdan yazmazsanız).

Her zaman üçüncü bir seçenek vardır ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

Seksi değil, ama dişleri var ( Tom Cruise ve The Firm filmi )

Seçim senin, ama şimdi biliyorsun ve bilgili biri olabilir.


5

Anonim delege adını verebilir :-)

Ve ikincisini şu şekilde yazabilirsiniz:

someList.ForEach(s => s.ToUpper())

Hangi tercih ve yazarak çok tasarruf sağlar.

Joachim'in dediği gibi, paralelliğin ikinci forma uygulanması daha kolaydır.


2

Sahnelerin arkasında, anonim delege gerçek bir yönteme dönüşür, böylece derleyici işlevi satır içine almayı seçmediyse ikinci seçenekle biraz ek yüke sahip olabilirsiniz. Ayrıca, anonim delege örneğinin gövdesi tarafından başvurulan yerel değişkenler, yeni bir yönteme derlendiğini gizlemek için derleyici hileleri nedeniyle doğada değişecektir. C # 'nin bu büyüyü nasıl yaptığı hakkında daha fazla bilgi:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

ForEach işlevi List genel sınıfı üyesidir.

Dahili kodu yeniden oluşturmak için aşağıdaki uzantı oluşturduk:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Sonuç olarak normal bir foreach (ya da isterseniz bir döngü) kullanıyoruz.

Öte yandan, bir delege işlevi kullanmak, bir işlevi tanımlamanın başka bir yoludur, bu kod:

delegate(string s) {
    <process the string>
}

şuna eşittir:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

veya labda ifadeleri kullanarak:

(s) => <process the string>

2

Tüm ForEach kapsamı (temsilci işlevi) tek bir kod satırı olarak kabul edilir (işlevi çağırır) ve kesme noktalarını ayarlayamaz veya koda adım atamazsınız. İşlenmeyen bir istisna oluşursa tüm blok işaretlenir.


2

List.ForEach () daha işlevsel olarak kabul edilir.

List.ForEach()ne yapmak istediğini söylüyor. foreach(item in list)ayrıca tam olarak nasıl yapılmasını istediğinizi de söylüyor. Bu , gelecekte nasıl bir kısmın List.ForEachuygulanmasını değiştirmekte serbesttir . Örneğin, .Net'in gelecekteki bir varsayım sürümü her zaman çalışabilirList.ForEach , bu noktada herkesin genellikle boşta oturan bir dizi işlemci çekirdeğine sahip olduğu varsayımı altında paralel .

Öte yandan, foreach (item in list)döngü üzerinde biraz daha fazla kontrol sağlar. Örneğin, öğelerin bir tür sıralı sırayla yineleneceğini bilirsiniz ve bir öğe bir koşulla karşılaşırsa kolayca ortadan kırılabilir.


Bu konuyla ilgili daha yeni bazı açıklamalara buradan ulaşabilirsiniz:

https://stackoverflow.com/a/529197/3043


1

Gösterdiğiniz ikinci yol, listedeki öğelerin her biri için temsilci yöntemini yürütmek için bir uzantı yöntemi kullanır.

Bu şekilde başka bir temsilci (= yöntem) çağrınız olur.

Ayrıca, for döngüsüyle listeyi yineleme imkanı vardır .


1

Dikkat edilmesi gereken bir şey, Generic .ForEach yönteminden nasıl çıkılacağıdır - bu tartışmaya bakın . Her ne kadar bağlantı bu yolun en hızlı olduğunu söylüyor gibi görünüyor. Neden olduğundan emin değilsiniz - derlendiklerinde eşdeğer olacaklarını düşünürdünüz ...


0

Bir yolu var, ben benim app yaptık:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Foreach öğesinden öğe olarak kullanabilirsiniz

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.