web-dev-qa-db-ja.com

コレクションが変更されました。列挙操作は実行できません

デバッガが接続されているときには発生しないように思われるため、このエラーの根本的な原因にはなりません。以下はそのコードです。

これはWindowsサービス内のWCFサーバーです。 NotifySubscribersメソッドは、データイベントがあるときはいつでも(ランダムな間隔で、しかしそれほど頻繁ではない - 1日に約800回)サービスによって呼び出されます。

Windowsフォームクライアントが購読すると、購読者IDが購読者辞書に追加され、クライアントが購読を解除すると、辞書から削除されます。このエラーは、クライアントが退会したとき(またはその後)に発生します。次に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);
        }
    }
}
803
cdonner

起こり得ることは、SignalDataがループ中に間接的にフード内の購読者辞書を変更し、そのメッセージにつながっていることです。変更することでこれを確認できます

foreach(Subscriber s in subscribers.Values)

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

私が正しいなら、問題は消えます

Subscribers.Values.ToList()を呼び出すと、foreachの始めにsubscribers.Valuesの値が別のリストにコピーされます。このリストにアクセスできるものは他に何もありません(変数名さえ持っていません!)ので、ループ内でそれを変更することはできません。

1430
JaredPar

購読者が購読を解除すると、列挙中に購読者のコレクションの内容が変更されます。

これを修正する方法はいくつかあります。1つは明示的な.ToList()を使うようにforループを変更することです。

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

私の考えでは、もっと効率的な方法は、「削除される」ものをすべて入れることを宣言する別のリストを用意することです。その後、(.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 );
}

これはあなたの問題を解決するだけでなく、あなたがあなたの辞書からリストを作り続けなければならないことからあなたを妨げるでしょう。任意の反復で削除されるサブスクライバのリストがリスト内の総数より少ないと仮定すると、これは速くなるはずです。しかし、もちろん、あなたの特定の使用状況に疑問がある場合は、それを確実にするためにプロファイルを作成してください。

58
x4000

購読者辞書をロックして、ループしているときに変更されないようにすることもできます。

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

なぜこのエラーですか?

一般に、.NETコレクションは、列挙と修正の同時実行をサポートしていません。列挙中にコレクションリストを変更しようとすると、例外が発生します。そのため、このエラーの背後にある問題は、同じものをループしている間はリストや辞書を変更できないことです。

解決策の一つ

キーのリストを使用して辞書を反復すると、辞書ではなくキーコレクションを反復しながら(およびそのキーコレクションを反復しながら)、辞書オブジェクトを変更できます。

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

これは この記事に関するブログ投稿 です。

そしてStackOverflowを深く掘り下げるには: このエラーが発生するのはなぜですか?

16
open and free

実際問題は私からあなたがリストから要素を取り除き、何も起こらなかったかのようにリストを読み続けることを期待しているということです。

あなたが本当にする必要があるのは終わりから始めて始めに戻ることです。リストから要素を削除しても、それを読み続けることができます。

4
luc.rg.roy

InvalidOperationException - / InvalidOperationExceptionが発生しました。 foreachループで「コレクションが変更された」と報告します。

オブジェクトが削除されたら、breakステートメントを使用してください。

例:

ArrayList list = new ArrayList(); 

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

私はこれのための多くの選択肢を見ましたが、私にとってこれは最高でした。

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

私は同じ問題を抱えていました、そして私が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);
}
2
Daniel Moreshet

さてそれで私が逆方向に反復するのを助けたものはそうしました。リストからエントリを削除しようとしていましたが、上方向に反復していましたが、エントリがもう存在していなかったため、ループが発生しました。

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

                            myList.RemoveAt(x);

                        }
1
Mark Aven

購読者辞書オブジェクトを同じタイプの一時辞書オブジェクトにコピーしてから、foreachループを使用して一時辞書オブジェクトを繰り返すことができます。

0
Rezoan

それが非常によく詳しく述べられた1つのリンクがあります、そして、解決策も与えられます。あなたが適切な解決策を得たらそれを試してみてください他の人が理解できるようにここに投稿してください。他の人がこれらの解決策を試すことができるように与えられた解決策は、投稿のようにして大丈夫です。

あなたは元のリンクを参照してください: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not -execute/

定義がEnumerable型、つまりコレクションを含むオブジェクトをシリアライズするために.NETシリアライゼーションクラスを使用すると、コーディングがマルチスレッドのシナリオの下である場合は、「コレクションは変更されました。一番下の原因は、直列化クラスが列挙子を介してコレクションを反復処理することです。そのため、問題はコレクションを変更しながらコレクションを反復処理しようとすることです。

1つ目の解決策は、Listオブジェクトに対する操作を一度に1つのスレッドからしか実行できないようにするための同期ソリューションとして、単純にlockを使用することです。明らかに、そのオブジェクトのコレクションをシリアル化したい場合は、それぞれに対してロックが適用されるというパフォーマンス上のペナルティが発生します。

ええと、マルチスレッドのシナリオを扱うことが便利になります。Net4.0。このシリアル化するCollectionフィールドの問題については、スレッドセーフでFIFOコレクションで、コードをロックフリーにするConcurrentQueue(Check MSDN)クラスの恩恵を受けることができます。

このクラスを使用すると、その単純さにおいて、Collection型をそれに置き換える必要があります。Enqueueを使用してConcurrentQueueの最後に要素を追加し、それらのロックコードを削除します。あるいは、作業中のシナリオでListなどのコレクションが必要な場合は、ConcurrentQueueを自分のフィールドに適応させるためのコードがさらにいくつか必要になります。

ところで、ConcurrentQueueには、コレクションのアトミックな消去を許可しない基本的なアルゴリズムのため、Clearメソッドがありません。あなた自身でそれをしなければならないので、最速の方法は置き換えのために新しい空のConcurrentQueueを作り直すことです。

0
Meghraj

したがって、この問題を解決する別の方法は、要素を削除して新しい辞書を作成し、削除したくない要素のみを追加してから、元の辞書を新しい辞書に置き換えることです。構造体を反復処理する回数が増えないため、これがあまり効率的な問題であるとは思わない。

0
ford prefect