List <T> .ForEach listesinin değiştirilmesine neden izin veriyor?


90

Kullanırsam:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

Addiçinde foreachbizim ayaklarının altındaki halıyı çekerek beri ben, mantıksal düşünün ki; (numaralama işlemi yürütmek olmayabilir Koleksiyon modifiye edilmiş) bir InvalidOperationException atar.

Ancak kullanırsam:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

Bir OutOfMemoryException atana kadar döngü yaparak kendini ayağından anında vurur.

Her zaman List.ForEach'in ya için foreachya da için bir ambalaj olduğunu düşündüğüm gibi, bu bana bir sürpriz olarak geliyor for.
Bu davranışın nasıl ve neden olduğuna dair bir açıklaması olan var mı?

(Sürekli olarak tekrarlanan bir Genel Liste için ForEach döngüsünden esinlenmiştir )


7
Katılıyorum. Bu - şüpheli. Bunu microsoft connect'e göndermenizi ve açıklama istemenizi tavsiye ederim.
TomTom

4
"Bu, List.ForEach'in sadece ya için foreachya da için bir ambalaj olduğunu düşündüğüm için bana bir sürpriz olarak geliyor for." Hala kullanabilir for. Bir fordöngüde aynı eylemi gerçekleştirebilir ve sonuç olarak aynı OutOfMemoryException öğesini oluşturabilirsiniz.
Anthony Pegram

Bu benim soruma dayanıyor: stackoverflow.com/q/9311272/132239 , ayrıntılara girdiği için SWeko'ya teşekkürler
Kasrak

Yanıtlar:


68

Çünkü bu ForEachnumaralandırıcısını kullanmaz yöntem, bir ile öğeler arasında döngüler fordöngü:

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]);
    }
}

(JustDecompile ile elde edilen kod)

Numaralandırıcı kullanılmadığından, listenin değişip değişmediğini asla kontrol foretmez _sizeve her yinelemede artırıldığı için döngünün son durumuna asla ulaşılmaz .


Evet ama nasıl _sizehesaplanır? Sadece önceden hesaplanmışsa, benim örneğim için sadece bir kez çalıştırılmalı. Belli ki bir şekilde yenilenmiş.
SWeko

7
Ekle yönteminde yenilenir -> this._items [this._size ++] = öğe;
Fabio

1
@SWeko, hesaplanmaz, her öğe eklendiğinde veya kaldırıldığında güncellenir.
Thomas Levesque

1
Bir yoktur _versionözel değişken List<T>o listeyi kendisi değiştirebilir operasyonlar güncellenme üzere bunun senaryoların bu tür tespit olabilir.
SWeko

Önce boyutu (int theSize = this._size) alıp sonra for döngüsü içinde kullanarak istisnaları önleyebilirsiniz.
Lazlow

14

List<T>.ForEachforiçeriden uygulanır , bu nedenle numaralandırıcı kullanmaz ve koleksiyonun değiştirilmesine izin verir.


6

List sınıfına eklenmiş ForEach dahili olarak doğrudan dahili üyelerine eklenmiş bir for döngüsü kullandığından, bunu .NET çerçevesi için kaynak kodunu indirerek görebilirsiniz.

http://referencesource.microsoft.com/netframework.aspx

Bir foreach döngüsü her şeyden önce bir derleyici optimizasyonudur, ancak aynı zamanda bir gözlemci olarak koleksiyona karşı işlemelidir - bu nedenle koleksiyon değiştirilirse bir istisna atar.


Ve @Thomas gönderisinin nasıl yenilendiğine dair yoruma cevap vermek için - ekleme çağrıldığında dahili üyeler yenilenir, bu yüzden değişikliklere ayak uydurabilir. Mevcut olandan daha düşük bir dizinde bir ekleme gerçekleştirecek olsaydınız, o öğenin ötesinde yinelendiği için asla o öğe üzerinde işlem yapmazsınız. Ama sonuna kadar eklediğiniz için işe yarıyor.
Mike Perrenoud

1
Evet, yalnızca 'örnek' yazdırılarak Addsatırı değiştirmek strings.Insert(0, s + "!"). Belgelerde bundan hiç bahsedilmemesi garip.
SWeko

Bence Microsoft, belgelerinde bulunan her uyarıyı sağlamanın neredeyse imkansız olduğunu fark etti - bu yüzden şimdi kaynak kodunu sağlıyorlar. Bunu dürüstçe daha iyi bir çözüm buluyorum, ancak bulduğum tek sorun, WF gibi ürünlerin o kadar hızlı güncellenmemesi - 4.x WF kaynak kodu hala mevcut değil.
Mike Perrenoud

4

Bu konuyu biliyoruz, ilk yazıldığı zaman bir gözetim oldu. Maalesef bunu değiştiremiyoruz çünkü artık bu daha önce çalışan kodun çalışmasını engelleyecektir:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

Eric Lippert'in belirttiği gibi, bu yöntemin kendisinin kullanışlılığı sorgulanabilir , bu nedenle onu Metro tarzı uygulamalar için (yani Windows 8 uygulamaları) için dahil etmedik.

David Kean (BCL Ekibi)


1
Bunun büyük bir kırılma değişikliği olacağını görüyorum, ancak yine de açık olmayan şekillerde başarısız olabilir ve bu asla iyi bir şey değildir. ForEach yönteminin basit bir yöntemden (veya orijinal listenin karıştırılması gerekmiyorsa foreach) üstün olduğu bir senaryo göremiyorum
SWeko
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.