1068

デバッガーが接続されている場合は発生しないように見えるため、このエラーの原因を突き止めることはできません。

コレクションが変更されました。列挙操作が実行されない可能性があります

以下はコードです。

これは、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);
        }
    }
}
4

16 に答える 16

1889

起こりそうなことはSignalData、ループ中にサブスクライバーの辞書を内部で間接的に変更し、そのメッセージにつながることです。変更することでこれを確認できます

foreach(Subscriber s in subscribers.Values)

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

私が正しければ、問題は消えます。

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

于 2009-03-03T02:10:17.173 に答える
124

サブスクライバーがサブスクライブを解除すると、列挙中にサブスクライバーのコレクションの内容が変更されます。

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
于 2009-03-03T02:13:12.563 に答える
77

私の意見では、より効率的な方法は、「削除する」ものを入れることを宣言する別のリストを作成することです。次に、メイン ループ (.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 );
}

これは問題を解決するだけでなく、辞書からリストを作成し続ける必要がなくなります。これは、そこに多くのサブスクライバーがいる場合にコストがかかります. 特定の繰り返しで削除されるサブスクライバーのリストがリストの合計数よりも少ないと仮定すると、これはより高速になるはずです。もちろん、特定の使用状況に疑問がある場合は、プロファイルを作成してください。

于 2009-03-03T06:58:46.510 に答える
44

なぜこのエラーですか?

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

解決策の1つ

キーのリストを使用してディクショナリを反復処理する場合、ディクショナリではなくキー コレクションを反復処理する (およびそのキー コレクションを反復処理する) ため、並行してディクショナリ オブジェクトを変更できます。

//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 の詳細については、このエラーが発生する理由を教えてください。

于 2014-11-11T12:12:12.777 に答える
6

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

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

元:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
于 2017-03-16T07:20:55.977 に答える
5

実際には、リストから要素を削除し、何も起こらなかったかのようにリストを読み続けることを期待しているように思えます。

あなたが本当にしなければならないことは、最後から始めて最初に戻ることです。リストから要素を削除しても、読み続けることができます。

于 2012-05-23T16:10:21.383 に答える
2

この方法は、関数がまだ実行中に再度呼び出された場合の並行性の状況をカバーする必要があります (アイテムは一度だけ使用する必要があります)。

 while (list.Count > 0)
 {
    string Item = list[0];
    list.RemoveAt(0);
 
    // do here what you need to do with item
 
 } 
 

アイテムの実行中に関数が呼び出された場合、アイテムは使用されるとすぐに削除されるため、最初から繰り返されません。小さなリストのパフォーマンスにはあまり影響しません。

于 2020-11-12T10:58:40.020 に答える
2

同じ問題があり、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);
}
于 2014-06-16T07:29:50.843 に答える
0

サブスクライバ ディクショナリ オブジェクトを同じタイプの一時ディクショナリ オブジェクトにコピーしてから、foreach ループを使用して一時ディクショナリ オブジェクトを繰り返すことができます。

于 2013-05-29T13:59:41.383 に答える