Collection was modified; enumeration operation may not execute

后端 未结 16 2610
南旧
南旧 2020-11-21 06:05

I can\'t get to the bottom of this error, because when the debugger is attached, it does not seem to occur.

Collection was modified; enumeration operatio

16条回答
  •  面向向阳花
    2020-11-21 06:56

    Here is a specific scenario that warrants a specialized approach:

    1. The Dictionary is enumerated frequently.
    2. The Dictionary is modified infrequently.

    In this scenario creating a copy of the Dictionary (or the Dictionary.Values) before every enumeration can be quite costly. My idea about solving this problem is to reuse the same cached copy in multiple enumerations, and watch an IEnumerator of the original Dictionary for exceptions. The enumerator will be cached along with the copied data, and interrogated before starting a new enumeration. In case of an exception the cached copy will be discarded, and a new one will be created. Here is my implementation of this idea:

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    
    public class EnumerableSnapshot : IEnumerable, IDisposable
    {
        private IEnumerable _source;
        private IEnumerator _enumerator;
        private ReadOnlyCollection _cached;
    
        public EnumerableSnapshot(IEnumerable source)
        {
            _source = source ?? throw new ArgumentNullException(nameof(source));
        }
    
        public IEnumerator GetEnumerator()
        {
            if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
            if (_enumerator == null)
            {
                _enumerator = _source.GetEnumerator();
                _cached = new ReadOnlyCollection(_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(_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 ToEnumerableSnapshot(
            this IEnumerable source) => new EnumerableSnapshot(source);
    }
    

    Usage example:

    private static IDictionary _subscribers;
    private static EnumerableSnapshot _subscribersSnapshot;
    
    //...(in the constructor)
    _subscribers = new Dictionary();
    _subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();
    
    // ...(elsewere)
    foreach (var subscriber in _subscribersSnapshot)
    {
        //...
    }
    

    Unfortunately this idea cannot be used currently with the class Dictionary in .NET Core 3.0, because this class does not throw a Collection was modified exception when enumerated and the methods Remove and Clear are invoked. All other containers I checked are behaving consistently. I checked systematically these classes: List, Collection, ObservableCollection, HashSet, SortedSet, Dictionary and SortedDictionary. Only the two aforementioned methods of the Dictionary class in .NET Core are not invalidating the enumeration.


    Update: I fixed the above problem by comparing also the lengths of the cached and the original collection. This fix assumes that the dictionary will be passed directly as an argument to the EnumerableSnapshot's constructor, and its identity will not be hidden by (for example) a projection like: dictionary.Select(e => e).ΤοEnumerableSnapshot().


    Important: The above class is not thread safe. It is intended to be used from code running exclusively in a single thread.

提交回复
热议问题