سؤال

لا يمكنني الوصول إلى الجزء السفلي من هذا الخطأ، لأنه عند إرفاق مصحح الأخطاء، لا يبدو أنه يحدث.أدناه هو الرمز.

هذا هو خادم WCF في خدمة Windows.يتم استدعاء الأسلوب NotifySubscribers بواسطة الخدمة عندما يكون هناك حدث بيانات (على فترات عشوائية، ولكن ليس في كثير من الأحيان - حوالي 800 مرة في اليوم).

عندما يشترك عميل Windows Forms، تتم إضافة معرف المشترك إلى قاموس المشتركين، وعندما يقوم العميل بإلغاء الاشتراك، يتم حذفه من القاموس.يحدث الخطأ عند (أو بعد) إلغاء اشتراك العميل.يبدو أنه في المرة التالية التي يتم فيها استدعاء أسلوب NotifySubscribers()، تفشل حلقة foreach() بسبب الخطأ في سطر الموضوع.تكتب الطريقة الخطأ في سجل التطبيق كما هو موضح في الكود أدناه.عند إرفاق مصحح أخطاء وإلغاء اشتراك العميل، يتم تنفيذ التعليمات البرمجية بشكل جيد.

هل ترى مشكلة في هذا الكود؟هل أحتاج إلى جعل القاموس آمنًا؟

[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);
        }
    }
}
هل كانت مفيدة؟

المحلول

ما يحدث على الأرجح هو أن SignalData يقوم بتغيير قاموس المشتركين بشكل غير مباشر أسفل الغطاء أثناء الحلقة ويؤدي إلى تلك الرسالة.يمكنك التحقق من ذلك عن طريق التغيير

foreach(Subscriber s in subscribers.Values)

ل

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

إذا كنت على حق، فإن المشكلة سوف تختفي

يؤدي استدعاء المشتركين.القيم.ToList() إلى نسخ قيم المشتركين.القيم إلى قائمة منفصلة في بداية عملية foreach.لا يوجد أي شيء آخر لديه حق الوصول إلى هذه القائمة (حتى أنها لا تحتوي على اسم متغير!)، لذا لا يمكن لأي شيء تعديلها داخل الحلقة.

نصائح أخرى

عند إلغاء الاشتراك مشترك كنت تقوم بتغيير محتويات مجموعة من المشتركين خلال التعداد.

وهناك عدة طرق لإصلاح هذا، واحد يجري تغيير لحلقة لاستخدام .ToList() صريح:

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

وهناك طريقة أكثر كفاءة، في رأيي، هو أن تكون قائمة أخرى أن تقوم بتعريف التي وضعت كل ما هو "لإزالتها" في. ثم بعد الانتهاء من حلقة الرئيسية الخاصة بك (بدون .ToList ())، يمكنك القيام حلقة أخرى على قائمة "يجب ازالتها"، وإزالة كل إدخال كما يحدث. حتى في صفك إضافة:

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

وبعد ذلك يمكنك تغييره إلى:

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

وهذا من شأنه ليس فقط حل مشكلتك، وسوف تمنعك من الحاجة إلى الحفاظ إنشاء قائمة من قاموسك، وهو أمر مكلف إذا كان هناك الكثير من المشتركين في هناك. وإذا افترضنا أن قائمة المشتركين إلى إزالتها في أي تكرار معين هو أقل من العدد الإجمالي في القائمة، وهذا ينبغي أن يكون أسرع. ولكن بالطبع لا تتردد في الشخصية أن تكون على يقين من أن هذا هو الحال إذا كان هناك أي شك في الوضع استخدام الخاص بك محددة.

ويمكنك أيضا قفل المشتركين الخاص قاموس لمنعه من تعديل كلما كونها يحلق:

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

لماذا هذا الخطأ؟

في مجموعات. صافي العامة لا تدعم يتم تعداد وتعديلها في نفس الوقت. إذا حاولت تعديل قائمة جمع أثناء تعداد، فإنه يثير استثناء. حتى قضية وراء هذا الخطأ، لا يمكننا تعديل قائمة / القاموس بينما نحن حلقات من خلال نفسه.

أحد الحلول

إذا كنا تكرار قاموس باستخدام قائمة مفاتيحها، بالتوازي يمكننا تعديل الكائن القاموس، ونحن بالتكرار من خلال جمع رئيسي و لا القاموس (وبالتكرار جمع الرئيسي).

مثال

//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;
}

وهنا هو < م> بلوق وظيفة حول هذا الحل.

وعن الغوص العميق في ستاكوفيرفلوو: لماذا يحدث هذا الخطأ

والواقع أن المشكلة يبدو لي أنك تقوم بإزالة عناصر من القائمة وأتوقع أن تستمر في قراءة قائمة وكأن شيئا لم يحدث.

وماذا كنت حقا بحاجة الى القيام به هو أن نبدأ من النهاية والعودة إلى البداية. حتى لو قمت بإزالة عناصر من القائمة التي سوف تكون قادرة على مواصلة القراءة.

استثناء_عملية_غير_صالحة- لقد حدث استثناء InvalidOperationException.يُبلغ عن "تم تعديل المجموعة" في حلقة foreach

استخدم عبارة Break، بمجرد إزالة الكائن.

السابق:

ArrayList list = new ArrayList(); 

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

وكان لي نفس المشكلة، وحلها عندما كنت حلقة for بدلا من 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);
}

ولقد شاهدت العديد من الخيارات لهذا ولكن بالنسبة لي كان هذا هو الأفضل.

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

وبعد ذلك ببساطة حلقة من خلال جمع.

كن على علم بأن ListItemCollection يمكن أن تحتوي على التكرارات. افتراضيا لا يوجد شيء مكررة منع إضافتها إلى المجموعة. لتجنب التكرارات يمكنك القيام بذلك:

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

وحسنا حتى ما ساعدني كان بالتكرار إلى الوراء. كنت أحاول أن إزالة إدخال من قائمة ولكن بالتكرار صعودا وثمل حلقة لأن دخول لم يعد له وجود:

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

                            myList.RemoveAt(x);

                        }

ويمكنك نسخ المشتركين الكائن القاموس لنفس نوع الكائن القاموس مؤقت ومن ثم تكرار الكائن القاموس مؤقت باستخدام حلقة Foreach.

وهكذا بطريقة مختلفة لحل هذه المشكلة ستكون بدلا من إزالة عناصر خلق قاموس جديد وفقط إضافة العناصر التي لم ترغب في إزالة ثم استبدال القاموس الأصلي مع واحدة جديدة. أنا لا أعتقد أن هذا هو الكثير جدا من مشكلة الكفاءة لأنه لا يزيد من عدد المرات التي أعاد على الهيكل.

وهناك وصلة واحدة حيث وضعت بشكل جيد للغاية ويعطى أيضا حل. تحاول ذلك إذا كنت حصلت على حل مناسب الرجاء نشر هنا البعض بحيث يمكن أن نفهم. حل معين على ما يرام ثم مثل هذا المنصب الآخر بحيث يمكن محاكمة هؤلاء الحل.

وبالنسبة لك مرجعية الرابط الأصلي: - <وأ href = "https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/" يختلط = "نوفولو noreferrer "> <لأ href =" https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not- تنفيذ / "يختلط =" noreferrer نوفولو "> https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

وعندما نستخدم الطبقات التسلسل صافي لتسلسل كائن حيث يحتوي تعريفه نوع Enumerable، أي جمع وسوف يكون من السهل الحصول على InvalidOperationException قائلا "مجموعة تم تعديلها. عملية التعداد قد يتم تنفيذ "حيث الترميز الخاص بك هو تحت سيناريوهات متعددة الصفحات. السبب القول هي أن الطبقات التسلسل وأعاد من خلال جمع عبر العداد، على هذا النحو، مشكلة يذهب إلى محاولة تكرار خلال جمع حين تعديله.

والحل الأول، يمكننا ببساطة استخدام قفل كحل تزامن لضمان أن العملية إلى كائن قائمة لا يمكن إلا أن يتم تنفيذها من موضوع واحد في وقت واحد.  من الواضح، سوف تحصل عقوبة الأداء إذا كنت ترغب في تسلسل مجموعة من هذا الكائن، ثم لكل منهما، سيتم تطبيق القفل.

حسنا، صافي 4.0 مما يجعل التعامل مع سيناريوهات متعددة خيوط قوية. لهذه المشكلة الحقل مجموعة تسلسل، وجدت أننا يمكن أن تأخذ فقط الاستفادة من ConcurrentQueue (راجع MSDN) فئة، وهو عبارة عن مجموعة آمن موضوع وFIFO ويجعل رمز قفل خالية.

<ع> استخدام هذه الفئة، في بساطته، والاشياء التي تحتاج إلى تعديل للالتعليمات البرمجية تقوم باستبدال نوع جمع مع ذلك، استخدام إدراج بقائمة الانتظار لإضافة عنصر إلى نهاية ConcurrentQueue، إزالة تلك رمز القفل. أو، إذا كان السيناريو الذي تعمل عليه القيام تتطلب الاشياء جمع مثل قائمة، وسوف تحتاج إلى قليل من الرمز إلى التكيف ConcurrentQueue في الحقول.

وراجع للشغل، ConcurrentQueue doesnât ديك أسلوب واضح بسبب الخوارزمية الأساسية التي تسمح للdoesnât تطهير بالذرة المجموعة. ولذلك عليك أن تفعل ذلك بنفسك، وأسرع وسيلة لإعادة خلق ConcurrentQueue جديدة فارغة عن بديل.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top