前言
歡迎關注【dotnet研習社】,今天我們聊聊一個基礎問題“集合已修改:可能無法執行枚舉操作”背后的設計。
在日常 C# 開發中,我們常常會操作集合(如 List<T>
、Dictionary<K,V>
等)。一個新手開發者極有可能遇到下面這個經典異常:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
這通常意味著你在 遍歷集合的過程中嘗試修改集合本身(添加或刪除元素),這是被禁止的。本文將深入剖析這個問題產生的原因,并分享常見的幾種 安全解決方案,幫助我們從容應對這一異常。
一、問題復現
來看一個簡單的例子:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };foreach (int num in numbers)
{if (num % 2 == 0){numbers.Remove(num); // 報錯!}
}
運行后會拋出異常:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
這是因為 foreach
在枚舉集合時,會維護一個內部狀態來防止在枚舉過程中破壞結構,一旦結構變動,就會拋出異常。
二、常見的正確做法
方法 1:倒序 for
循環移除元素
適用于 List<T>
這種支持索引的集合:
for (int i = numbers.Count - 1; i >= 0; i--)
{if (numbers[i] % 2 == 0){numbers.RemoveAt(i);}
}
? 倒序循環可以避免因為索引變動導致的跳過元素或崩潰。
方法 2:使用 LINQ 的 Where + ToList()
創建副本遍歷
foreach (var num in numbers.Where(n => n % 2 == 0).ToList())
{numbers.Remove(num);
}
?
ToList()
會創建一個集合副本,這樣你就可以安全地對原集合進行修改了。
方法 3:臨時列表收集要移除的項,二次遍歷移除
var toRemove = new List<int>();
foreach (var num in numbers)
{if (num % 2 == 0){toRemove.Add(num);}
}
foreach (var num in toRemove)
{numbers.Remove(num);
}
? 這種方法安全可靠,尤其適合處理復雜條件刪除場景。
方法 4:直接使用 List<T>.RemoveAll()
這是最簡潔的一種方式:
numbers.RemoveAll(n => n % 2 == 0);
? 適用于只需要從集合中刪除符合某個條件的元素場景。
三、適用于不同集合類型的說明
集合類型 | 遍歷時可修改? | 推薦處理方式 |
---|---|---|
List<T> | ? | 倒序/臨時列表/RemoveAll |
Dictionary<K,V> | ? | ToList()拷貝鍵值對后操作 |
HashSet<T> | ? | 先收集,后統一移除 |
ConcurrentBag<T> | ? | 支持并發讀寫,無需額外處理 |
如果正在開發多線程程序,強烈推薦使用線程安全集合,如
ConcurrentDictionary<K,V>
、ConcurrentQueue<T>
等。
四、深入理解為何不能修改
foreach
的底層是使用了IEnumerator
;- 當修改集合時(比如
Remove()
),集合的version
字段會更新; IEnumerator
檢測到版本變動后,會拋出InvalidOperationException
,以防止出現難以調試的數據錯誤。
我們可以通過查看 .NET 源碼中關于 List<T>
、IEnumerator
以及 version
字段的真實實現,驗證上面的描述并深入理解:
- https://github.com/dotnet/runtime
- 關鍵路徑
src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs
在該文件中可以看到 List<T>
的實現細節:
示例:List<T>.Enumerator.MoveNext()
中的 version
檢查
public bool MoveNext()
{List<T> localList = list;if (version == localList._version && (index < localList._size)){current = localList._items[index++];return true;}return MoveNextRare();
}
而在 MoveNextRare()
中可以看到拋出異常的邏輯:
private bool MoveNextRare()
{if (version != list._version){ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();}index = list._size + 1;current = default!;return false;
}
說明只要外部在枚舉過程中修改了集合(導致 _version
改變),枚舉器就會感知并拋出異常。
這種機制的目的是保護開發者避免數據一致性錯誤,雖然它帶來了限制,但也增強了代碼的健壯性。
五、總結一句話
遍歷集合時不要修改集合本身。
如果需要修改,請先復制副本或延后批量處理,不要在
foreach
中直接Add
或Remove
。
六、附加:通用工具方法(刪除滿足條件的元素)
我們可以封裝一個更通用的方法,供多處復用:
public static void SafeRemove<T>(List<T> list, Func<T, bool> predicate)
{list.RemoveAll(predicate);
}
使用方式:
SafeRemove(numbers, n => n % 2 == 0);
七、延伸閱讀推薦
- .NET 源碼解析:List 是如何防止你在遍歷中修改它的?
- .NET GitHub 源碼結構導覽
- Stack Overflow 高票回答:Why does modifying a list while iterating cause an exception?