Koleksiyon değiştirildi; numaralandırma işlemi yürütülemeyebilir


910

Hata ayıklayıcı eklendiğinde, bu görünmüyor çünkü bu hatanın altına alamıyorum. Kod aşağıdadır.

Bu, bir Windows hizmetindeki bir WCF sunucusudur. NotifySubscribers yöntemi, bir veri olayı olduğunda (rastgele aralıklarla, ancak çok sık değil - günde yaklaşık 800 kez) hizmet tarafından çağrılır.

Bir Windows Forms istemcisi abone olduğunda, abone kimliği abone sözlüğüne eklenir ve istemci aboneliği iptal ettiğinde sözlükten silinir. Hata, bir istemci aboneliğini iptal ettiğinde (veya sonrasında) oluşur. NotifySubscribers () yöntemi bir sonraki çağrıldığında, foreach () döngüsü konu satırındaki hatayla başarısız olur. Yöntem, hatayı aşağıdaki kodda gösterildiği gibi uygulama günlüğüne yazar. Bir hata ayıklayıcı iliştirildiğinde ve istemci aboneliği iptal ettiğinde, kod iyi yürütülür.

Bu kodla ilgili bir sorun görüyor musunuz? Sözlüğü iş parçacığı için güvenli hale getirmem gerekir mi?

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
    private static IDictionary<Guid, Subscriber> subscribers;

    public SubscriptionServer()
    {            
        subscribers = new Dictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        foreach(Subscriber s in subscribers.Values)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                UnsubscribeEvent(s.ClientId);
            }
        }
    }


    public Guid SubscribeEvent(string clientDescription)
    {
        Subscriber subscriber = new Subscriber();
        subscriber.Callback = OperationContext.Current.
                GetCallbackChannel<IDCSCallback>();

        subscribers.Add(subscriber.ClientId, subscriber);

        return subscriber.ClientId;
    }


    public void UnsubscribeEvent(Guid clientId)
    {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                    e.Message);
        }
    }
}

benim durumumda bir teminat etkisi oldu çünkü ben işlem sırasında değiştirilmiş birkaç .Include ("tablo") kullanıyordum - kodu okurken çok açık değil. Ancak bu Includes gerekli değildi şanslıydım (evet! eski, bakımsız kodu) ve ben sadece onları kaldırarak sorunumu çözdü
Adi

Yanıtlar:


1631

Muhtemelen, SignalData'nın döngü sırasında kaputun altındaki abone sözlüğünü dolaylı olarak değiştirmesi ve bu mesaja yol açmasıdır. Bunu değiştirerek doğrulayabilirsiniz.

foreach(Subscriber s in subscribers.Values)

için

foreach(Subscriber s in subscribers.Values.ToList())

Haklıysam, sorun ortadan kalkacak

Çağrıldığında, subscribers.Values.ToList()öğesinin başlangıcındaki değerleri subscribers.Valuesayrı bir listeye kopyalar foreach. Başka hiçbir şey bu listeye erişemez (değişken bir adı bile yoktur!), Bu nedenle döngü içinde hiçbir şey değiştiremez.


14
BTW .ToList (), .NET 2.0 uygulamalarıyla uyumlu olmayan System.Core dll dosyasında bulunur. Bu nedenle, hedef uygulamanızı .Net 3.5 olarak değiştirmeniz gerekebilir
mishal153

60
Neden bir ToList yaptığınızı ve neden her şeyi düzelttiğini anlamıyorum
PositiveGuy

204
@CoffeeAddict: Sorun, döngü subscribers.Valuesiçinde değiştirilmesidir foreach. Çağrıldığında, subscribers.Values.ToList()öğesinin başlangıcındaki değerleri subscribers.Valuesayrı bir listeye kopyalar foreach. Başka hiçbir şeyin bu listeye erişimi yoktur (değişken bir adı bile yoktur!) , Bu nedenle döngü içinde hiçbir şey değiştiremez.
BlueRaja - Danny Pflughoeft

31
ToListKoleksiyon ToListyürütülürken koleksiyon değiştirilirse de atılabileceğini unutmayın .
Sriram Sakthivel

16
Sorunun çözülmediğini, ancak çoğaltılmasını zorlaştırdığından eminim. ToListatomik bir işlem değil. Daha komik olan, temel olarak öğeleri yeni bir liste örneğine kopyalamak ToListiçin kendi başına yapar, bu da ek (daha hızlı olmasına rağmen) bir yineleme ekleyerek bir sorunu foreachdüzelttiğiniz anlamına gelir . foreachforeach
Groo

113

Bir abone aboneliği iptal ettiğinde, numaralandırma sırasında Abone koleksiyonunun içeriğini değiştirirsiniz.

Bunu düzeltmenin birkaç yolu vardır, bunlardan biri for döngüsünü açık bir şekilde kullanacak şekilde değiştirmektir .ToList():

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...

64

Bence daha etkili bir yol, “kaldırılması” gereken her şeyi koyduğunuzu beyan ettiğiniz başka bir listeye sahip olmaktır. Daha sonra ana döngünüzü (.ToList () olmadan) bitirdikten sonra, "kaldırılacak" listesinde başka bir döngü gerçekleştirerek her girdiyi olduğu gibi kaldırırsınız. Sınıfınıza şunları eklersiniz:

private List<Guid> toBeRemoved = new List<Guid>();

Sonra şu şekilde değiştirin:

public void NotifySubscribers(DataRecord sr)
{
    toBeRemoved.Clear();

    ...your unchanged code skipped...

   foreach ( Guid clientId in toBeRemoved )
   {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                e.Message);
        }
   }
}

...your unchanged code skipped...

public void UnsubscribeEvent(Guid clientId)
{
    toBeRemoved.Add( clientId );
}

Bu sadece sorununuzu çözmekle kalmayacak, sözlükten bir liste oluşturmaya devam etmenizi engelleyecektir, bu da çok fazla abone varsa pahalıdır. Herhangi bir yinelemede kaldırılacak abone listesinin listedeki toplam sayıdan daha düşük olduğu varsayılarak, bu daha hızlı olmalıdır. Ancak elbette, özel kullanım durumunuzda herhangi bir şüphe varsa, durumun böyle olduğundan emin olmak için profil oluşturmaktan çekinmeyin.


9
Daha büyük koleksiyonlarla çalışıyorsanız, bunun dikkate değer olmasını bekliyorum. Eğer küçük olsaydı, muhtemelen sadece ToList ve devam edecektim.
Karl Kieninger

42

Ayrıca, döngü oluşturulduğunda değiştirilmesini önlemek için abone sözlüğünüzü kilitleyebilirsiniz:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }

2
Bu tam bir örnek mi? MarkerFrequencies adlı genel bir sözlük <dize, int> içeren bir sınıf (aşağıda _dictionary obj) var, ama bunu yapmak anında çökmesini çözmedi: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> çifti _dictionary.MarkerFrequencies) {...}}
Jon Coombs

4
@JCoombs, muhtemelen MarkerFrequencieskilidi kendi içinde sözlüğü yeniden atamanız , yani orijinal örneğin artık kilitlenmemiş olması mümkündür. Ayrıca kullanmayı deneyin foryerine ait foreachBakınız bu ve bu . Bunu çözüp çözmediğini bana bildirin.
Mohammad Sepahvand

1
Sonunda bu hatayı düzeltmek için etrafta dolaştım ve bu ipuçları yardımcı oldu - teşekkürler! Veri setim küçüktü ve bu işlem sadece bir UI ekran güncellemesiydi, bu yüzden bir kopyayı tekrarlamak en iyi görünüyordu. (Ben de her iki iş parçacığında MarkerFrequencies kilitledikten sonra foreach yerine kullanarak denedim. Bu çökmesini engelledi ama daha fazla hata ayıklama çalışması gerektiriyor gibiydi. Ve daha karmaşıklık getirdi. Burada iki iş parçacığı olması için tek neden kullanıcı iptal etmek Bu işlem bir kullanıcı tarafından doğrudan değiştirilmez.)
Jon Coombs

9
Sorun, büyük uygulamalar için kilitlerin büyük bir performans isabeti olabilmesidir - System.Collections.Concurrentad alanında bir koleksiyon kullanmak daha iyidir .
BrainSlugs83

28

Neden bu hata?

Genel olarak .Net koleksiyonları aynı anda numaralandırılmayı ve değiştirilmeyi desteklemez. Numaralandırma sırasında koleksiyon listesini değiştirmeye çalışırsanız, bir istisna oluşturur. Yani bu hatanın arkasındaki sorun, aynı döngüye girerken listeyi / sözlüğü değiştiremeyiz.

Çözümlerden biri

Anahtarlarının bir listesini kullanarak bir sözlüğü tekrarlarsak, sözlükte değil, anahtar koleksiyonunda yineleme yaptığımızdan (ve anahtar koleksiyonunu yinelediğinden) paralel olarak sözlük nesnesini değiştirebiliriz.

Misal

//get key collection from dictionary into a list to loop through
List<int> keys = new List<int>(Dictionary.Keys);

// iterating key collection using a simple for-each loop
foreach (int key in keys)
{
  // Now we can perform any modification with values of the dictionary.
  Dictionary[key] = Dictionary[key] - 1;
}

İşte bu çözüm hakkında bir blog yazısı .

Ve StackOverflow derin bir dalış için: Neden bu hata oluşur?


Teşekkür ederim! Neden gerçekleştiğinin açık bir açıklaması ve hemen başvurumda bunu çözmenin yolunu verdi.
Kim Crosser

5

Aslında sorun bana öyle geliyor ki öğeleri listeden kaldırıyorsunuz ve hiçbir şey olmamış gibi listeyi okumaya devam etmeyi umuyorsunuz.

Gerçekten yapmanız gereken, baştan başlayıp en baştan başlamanızdır. Listeden öğeleri kaldırsanız bile okumaya devam edebileceksiniz.


Bunun nasıl bir fark yaratacağını görmüyorum? Listenin ortasındaki bir öğeyi kaldırırsanız, erişmeye çalıştığı öğe bulunamadığından hata atar mı?
Zapnologica

4
@Zapnologica farkı - listeyi numaralandırmazsınız - for / each yerine, for / next yapıyorsunuz ve tamsayı ile erişiyorsunuz - a'daki bir listeyi kesinlikle değiştirebilirsiniz. for / next loop, ama asla for / her loop için (çünkü / her numaralandırma için ) - ayrıca, sayaçlarınızı ayarlamak için ekstra mantığınız varsa, for / next içinde ileri doğru da yapabilirsiniz.
BrainSlugs83

4

InvalidOperationException- Bir InvalidOperationException oluştu. Bir foreach döngüsü içinde bir "koleksiyon değiştirildi" bildiriyor

Break deyimini kullanın, Nesne kaldırıldıktan sonra.

örn:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}

Listenizin kaldırılması gereken en az bir öğe olduğunu biliyorsanız iyi ve basit bir çözüm.
Tawab Wakil

3

Aynı sorunu yaşadım ve bunun foryerine bir döngü kullandığımda çözüldü foreach.

// foreach (var item in itemsToBeLast)
for (int i = 0; i < itemsToBeLast.Count; i++)
{
    var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach);

   if (matchingItem != null)
   {
      itemsToBeLast.Remove(matchingItem);
      continue;
   }
   allItems.Add(itemsToBeLast[i]);// (attachDetachItem);
}

11
Bu kod yanlıştır ve herhangi bir öğe kaldırılacaksa koleksiyondaki bazı öğeleri atlar. Örneğin: var arr = ["a", "b", "c"] var ve ilk yinelemede (i = 0) 0 konumunda elemanı kaldırıyorsunuz (eleman "a"). Bundan sonra tüm dizi elemanları bir konum yukarı hareket edecek ve dizi ["b", "c"] olacaktır. Böylece, bir sonraki yinelemede (i = 1) "b" değil "1" konumunda olan elemanı kontrol edeceksiniz. Bu yanlış. Bunu düzeltmek için aşağıdan yukarıya doğru hareket
etmelisin

3

Tamam, bana yardımcı olan şey geriye doğru yinelemekti. Bir listeden bir girişi kaldırmaya çalışıyordum ama yukarı doğru yineleme ve giriş artık mevcut olmadığı için döngü berbat:

for (int x = myList.Count - 1; x > -1; x--)
                        {

                            myList.RemoveAt(x);

                        }

2

Bunun için birçok seçenek gördüm ama bana göre bu en iyisiydi.

ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

Sonra sadece koleksiyon içinde döngü.

ListItemCollection öğesinin yinelenen öğeler içerebileceğini unutmayın. Varsayılan olarak, koleksiyona kopyaların eklenmesini engelleyen hiçbir şey yoktur. Yinelemeleri önlemek için şunları yapabilirsiniz:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }

Bu kod, yinelenenlerin veritabanına girmesini nasıl önler. Benzer bir şey yazdım ve liste kutusundan yeni kullanıcılar eklediğimde, yanlışlıkla listede bulunan birini seçtiysem, yinelenen bir giriş oluşturacak. @Mike ile ilgili herhangi bir öneriniz var mı?
Jamie

1

En kötü durumda kabul edilen cevap kesin değildir ve yanlıştır. Sırasında değişiklikler yapılırsaToList() , yine de bir hatayla karşılaşabilirsiniz. Ayrıca lock, bir kamu üyeniz varsa, hangi performans ve iş parçacığı güvenliğinin göz önünde bulundurulması gerektiği, değişmez türleri kullanarak uygun bir çözüm olabilir .

Genel olarak, değiştirilemez bir tür, oluşturulduktan sonra durumunu değiştiremeyeceğiniz anlamına gelir. Yani kodunuz şöyle görünmelidir:

public class SubscriptionServer : ISubscriptionServer
{
    private static ImmutableDictionary<Guid, Subscriber> subscribers = ImmutableDictionary<Guid, Subscriber>.Empty;
    public void SubscribeEvent(string id)
    {
        subscribers = subscribers.Add(Guid.NewGuid(), new Subscriber());
    }
    public void NotifyEvent()
    {
        foreach(var sub in subscribers.Values)
        {
            //.....This is always safe
        }
    }
    //.........
}

Bu, herkese açık bir üyeniz varsa özellikle yararlı olabilir. Diğer sınıflar, foreachdeğiştirilen koleksiyon hakkında endişelenmeden her zaman değişmez tiplerde olabilir .


1

Özel bir yaklaşımı garanti eden özel bir senaryo:

  1. DictionarySık sık numaralandırılan.
  2. DictionarySeyrek modifiye edilir.

Bu senaryoda Dictionary, Dictionary.Valuesher numaralandırmadan önce (veya ) bir kopyasını oluşturmak oldukça maliyetli olabilir. Bu sorunu çözme konusundaki fikrim, aynı önbelleğe alınmış kopyayı birden çok numaralandırmada yeniden kullanmak ve istisnalar IEnumeratoriçin bir orijinali izlemek Dictionary. Numaralandırıcı kopyalanan verilerle birlikte önbelleğe alınacak ve yeni bir numaralandırmaya başlamadan önce sorgulanacaktır. İstisna durumunda, önbelleğe alınmış kopya atılır ve yeni bir kopya oluşturulur. İşte bu fikri benim uygulamam:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

public class EnumerableSnapshot<T> : IEnumerable<T>, IDisposable
{
    private IEnumerable<T> _source;
    private IEnumerator<T> _enumerator;
    private ReadOnlyCollection<T> _cached;

    public EnumerableSnapshot(IEnumerable<T> source)
    {
        _source = source ?? throw new ArgumentNullException(nameof(source));
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
        if (_enumerator == null)
        {
            _enumerator = _source.GetEnumerator();
            _cached = new ReadOnlyCollection<T>(_source.ToArray());
        }
        else
        {
            var modified = false;
            if (_source is ICollection collection) // C# 7 syntax
            {
                modified = _cached.Count != collection.Count;
            }
            if (!modified)
            {
                try
                {
                    _enumerator.MoveNext();
                }
                catch (InvalidOperationException)
                {
                    modified = true;
                }
            }
            if (modified)
            {
                _enumerator.Dispose();
                _enumerator = _source.GetEnumerator();
                _cached = new ReadOnlyCollection<T>(_source.ToArray());
            }
        }
        return _cached.GetEnumerator();
    }

    public void Dispose()
    {
        _enumerator?.Dispose();
        _enumerator = null;
        _cached = null;
        _source = null;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public static class EnumerableSnapshotExtensions
{
    public static EnumerableSnapshot<T> ToEnumerableSnapshot<T>(
        this IEnumerable<T> source) => new EnumerableSnapshot<T>(source);
}

Kullanım örneği:

private static IDictionary<Guid, Subscriber> _subscribers;
private static EnumerableSnapshot<Subscriber> _subscribersSnapshot;

//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();

// ...(elsewere)
foreach (var subscriber in _subscribersSnapshot)
{
    //...
}

Ne yazık ki bu fikir şu anda Dictionary.NET Core 3.0'daki sınıfla kullanılamıyor , çünkü bu sınıf numaralandırılmadığında bir Koleksiyon değiştirilmedi özel durumu değiştirildi ve yöntemler Removeve Clearçağrıldı. Kontrol ettiğim diğer tüm kaplar tutarlı davranıyor. Ben sistematik olarak bu sınıfları kontrol: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>ve SortedDictionary<T,V>. Dictionary.NET Core'daki sınıfın yalnızca yukarıda belirtilen iki yöntemi numaralandırmayı geçersiz kılmaz.


Güncelleme: Yukarıdaki sorunu önbellek ve orijinal koleksiyonun uzunluklarını da karşılaştırarak çözdüm. Bu düzeltme, sözlüğün doğrudan EnumerableSnapshot'yapıcısına bir argüman olarak iletileceğini ve kimliğinin şöyle bir projeksiyonla (örneğin) gizlenmeyeceğini varsayar dictionary.Select(e => e).ΤοEnumerableSnapshot().


Önemli: Yukarıdaki sınıf iş parçacığı için güvenli değildir . Yalnızca tek bir iş parçacığında çalışan koddan kullanılmak üzere tasarlanmıştır.


0

Abone sözlüğü nesnesini aynı türden geçici sözlük nesnesine kopyalayabilir ve daha sonra foreach döngüsünü kullanarak geçici sözlük nesnesini yineleyebilirsiniz.


(Bu gönderi soruya kaliteli bir cevap vermiyor gibi görünüyor . Lütfen ya cevabınızı düzenleyin ya da sadece soruya yorum olarak gönderin).
sɐunıɔ ןɐ qɐp

0

Bu nedenle, bu sorunu çözmenin farklı bir yolu, yeni bir sözlük oluşturmak ve yalnızca kaldırmak istemediğiniz öğeleri eklemek yerine orijinal sözlüğü yenisiyle değiştirmektir. Bunun çok fazla bir verimlilik sorunu olduğunu düşünmüyorum çünkü yapı üzerinde yineleme sayısını artırmıyor.


0

Çok iyi işlendiği ve çözümün verildiği bir bağlantı var. Uygun bir çözümünüz varsa deneyin, lütfen diğerlerinin anlayabilmesi için buraya gönderin. Verilen çözüm sonra yazı gibi tamam diğer böylece bu çözümü deneyebilirsiniz.

referans için orijinal bağlantı: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

.Net Serialization sınıflarını, tanımının Enumerable türü, yani koleksiyonu içerdiği bir nesneyi serileştirmek için kullandığımızda, kodlamanız çok iş parçacıklı senaryoların altında olduğunda, "Koleksiyon değiştirildi; numaralandırma işlemi yürütülmeyebilir" diyerek InvalidOperationException'ı kolayca elde edersiniz. En temel neden, serileştirme sınıflarının numaralandırıcı aracılığıyla toplama yoluyla yinelenmesidir, bu nedenle sorun, bir koleksiyonu değiştirirken yinelemeye çalışmaktır.

İlk çözüm olarak, List nesnesine yapılan işlemin aynı anda yalnızca bir iş parçacığından yürütülebilmesini sağlamak için kilidi bir senkronizasyon çözümü olarak kullanabiliriz. Açıkçası, o nesnenin bir koleksiyonunu serileştirmek istiyorsanız, o zaman her biri için kilidin uygulanacağı performans cezası alacaksınız.

Çok iş parçacıklı senaryolarla uğraşmayı kullanışlı hale getiren Net 4.0. Bu serileştirme Koleksiyon alanı sorunu için, sadece bir iş parçacığı ve FIFO koleksiyonu olan ve kodu kilitsiz hale getiren ConcurrentQueue (Check MSDN) sınıfından yararlanabileceğimizi gördüm.

Bu sınıfı kullanarak, kodunuz için değiştirmeniz gereken şeyler Koleksiyon türünü onunla değiştiriyor, ConcurrentQueue sonuna bir öğe eklemek için Enqueue kullanın, bu kilit kodunu kaldırın. Ya da üzerinde çalıştığınız senaryo Liste gibi toplama öğeleri gerektiriyorsa, ConcurrentQueue'yu alanlarınıza uyarlamak için birkaç koda daha ihtiyacınız olacaktır.

BTW, ConcurrentQueue, koleksiyonun atomik olarak temizlenmesine izin vermeyen altta yatan algoritma nedeniyle Net bir yöntemi yoktur. bu yüzden bunu kendiniz yapmanız gerekir, en hızlı yol, değiştirme için yeni bir boş ConcurrentQueue yeniden oluşturmaktır.


-1

Ben şahsen bu Linq's .Remove (madde) ile açık bir dbcontext olduğu yabancı tanıdık kodunda koştu. Öğe (ler) ilk kez yineleme kaldırıldı çünkü hata ayıklama bulma bir cehennem vardı ve geri adım ve yeniden adım atmaya çalıştım, ben aynı bağlamda olduğu için koleksiyon boştu!

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.