Occasionally I have need for a collection of objects in which each object is referenced weakly (meaning the GC is allowed to collect it even when the object is in the collection). Traditionally, I would use a simple List<WeakReference<MyObject>>, occasionally purging any WeakReference whose target object had been garbage-collected (typically as the list was iterated or updated).I've found that the List<WeakReference<MyObject>> pattern has two major downsides:The code to purge the expired WeakReference objects is duplicated in all the places that use this pattern. It would be more convenient if this purging was handled automatically by the collection itself.Every WeakReference is its own heap-allocated object. If you want to store thousands of weakly-referenced objects, then that's thousands of additional objects that get allocated on the heap.One could also use ConditionalWeakTable, but that class doesn't support iteration, which is crucial for my purposes.What follows is my attempt at creating a WeakCollection<T> class which addresses both of those issues. I'd really appreciate it if anyone could look over my implementation and point out anything I'm being stupid about or important details I'm missing. I also welcome any general code-review type advice.Once particular piece of advice I'm looking for is how best to detect whether the collection is modified during enumeration, so that it can throw an exception.A few key details about my WeakCollection<T> class:The tracking and removal of dead objects is completely hidden to the application. From the application's perspective, once an object is garbage-collected, it is instantly gone from the collection.Instead of using .NET's WeakReference<T> class to store each object, it allocates a GCHandle (which is a value type) for every object stored in the collection.As the collection is updated or iterated, it automatically checks for any dead objects and removes their corresponding GCHandle values. The Remove, Contains, CopyTo, and GetEnumerator methods do this as they iterate. The Add method does this once the number of contained objects exceeds twice the number that existed after the last purge, or once the capacity of the internal List<GCHandle> object is filled. There's also a Purge method that can be called manually.This is not, and is not intended to be, thread-safe.I've put a GC.KeepAlive(this) call at the end of every method to prevent the finalizer from running while the method executes, but I'm not 100% sure it's required in all cases. (But can't hurt, right?)/// <summary>/// Stores a collection of weakly-referenced objects, allowing the objects/// in this collection to be garbage-collected. Garbage-collected objects/// will be immediately removed from the collection./// </summary>public class WeakCollection<T> : ICollection<T>    where T : class{    private readonly List<GCHandle> _handles;    private readonly bool _trackResurrection;    private int _countAfterLastPurge;    /// <summary>    /// Creates a new WeakCollection.    /// </summary>    /// <param name=capacity>The initial number of objects that the WeakCollection can hold before resizing.</param>    /// <param name=trackResurrection>If true, the object continues to be tracked even after it is finalized.</param>    public WeakCollection(int capacity = 8, bool trackResurrection = false)    {        if(capacity <= 0)            throw new ArgumentOutOfRangeException(nameof(capacity));        _handles = new List<GCHandle>(capacity);        _trackResurrection = trackResurrection;    }    /// <summary>    /// Frees the handles allocated for this WeakCollection.    /// </summary>    ~WeakCollection()    {        for(int i = 0; i < _handles.Count; i++)            _handles[i].Free();    }    /// <summary>    /// Returns the approximate number of objects stored in this WeakCollection.    /// The will never be less than the actual number of living objects, but may    /// be larger if some objects have been garbage-collected.    /// </summary>    /// <param name=purge>    /// If true, any garbage-collected objects will be purged before returning a value.    /// Even if this is true, this method may still return a higher-than-living value    /// if any objects are garbage-collected while this method is executing.    /// </param>    public int GetCount(bool purge)    {        if(purge)            Purge();        int count = _handles.Count;        GC.KeepAlive(this);        return count;    }    /// <summary>    /// Adds the specified object to this WeakCollection.    /// </summary>    /// <remarks>    /// This will occasionally purge the tracking memory for any garbage-collected objects.    /// </remarks>    public void Add(T obj)    {        if(obj == null)            throw new ArgumentNullException(nameof(obj));        if(_handles.Count == _handles.Capacity || _handles.Count + 1 > _countAfterLastPurge * 2)            Purge();        GCHandleType handleType = _trackResurrection ? GCHandleType.WeakTrackResurrection : GCHandleType.Weak;        GCHandle handle = GCHandle.Alloc(obj, handleType);        _handles.Add(handle);        GC.KeepAlive(this);    }    /// <summary>    /// Removes one occurrence of the specified object from this WeakCollection.    /// </summary>    /// <returns>True if the specified object was found and thus was removed.</returns>    /// <remarks>    /// This will automatically purge some objects as the collection is iterated while searching for the passed object.    /// </remarks>    public bool Remove(T obj)    {        if(obj == null)            throw new ArgumentNullException(nameof(obj));        bool found = false;        for(int i = 0; i < _handles.Count;)        {            GCHandle handle = _handles[i];            object target = handle.Target;            if(target == obj)            {                found = true;                handle.Free();                _handles.RemoveAt(i);                _countAfterLastPurge--;                break;            }            else if(target == null)            {                handle.Free();                _handles.RemoveAt(i);                _countAfterLastPurge--;            }            else            {                i++;            }        }        GC.KeepAlive(this);        return found;    }    /// <summary>    /// Removes all objects from this WeakCollection.    /// </summary>    public void Clear()    {        for(int i = 0; i < _handles.Count; i++)            _handles[i].Free();        _handles.Clear();        GC.KeepAlive(this);    }    /// <summary>    /// Returns whether this WeakCollection contains the specified object.    /// </summary>    /// <remarks>    /// This will automatically purge some objects as the collection is iterated while searching for the passed object.    /// </remarks>    public bool Contains(T obj)    {        if(obj == null)            throw new ArgumentNullException(nameof(obj));        bool found = false;        for(int i = 0; i < _handles.Count;)        {            GCHandle handle = _handles[i];            object target = handle.Target;            if(target == obj)            {                found = true;                break;            }            else if(target == null)            {                handle.Free();                _handles.RemoveAt(i);                _countAfterLastPurge--;            }            else            {                i++;            }        }        GC.KeepAlive(this);        return found;    }    /// <summary>    /// Returns an enumerator that iterates through all of the living objects in this WeakCollection.    /// </summary>    /// <remarks>    /// This will purge the tracking memory for any garbage-collected objects as they are iterated.    /// </remarks>    IEnumerator IEnumerable.GetEnumerator()    {        return GetEnumerator();    }    /// <summary>    /// Returns an enumerator that iterates through all of the living objects in this WeakCollection.    /// </summary>    /// <remarks>    /// This will purge the tracking memory for any garbage-collected objects as they are iterated.    /// </remarks>    public IEnumerator<T> GetEnumerator()    {        for(int i = 0; i < _handles.Count;)        {            GCHandle handle = _handles[i];            object target = handle.Target;            if(target == null)            {                handle.Free();                _handles.RemoveAt(i);            }            else            {                yield return (T)target;                i++;            }        }        _countAfterLastPurge = _handles.Count;        GC.KeepAlive(this);    }    /// <summary>    /// Copies the living objects stored in this WeakCollection to the specified array.    /// </summary>    /// <remarks>    /// This will purge the tracking memory for any garbage-collected objects.    /// </remarks>    public void CopyTo(T[] array, int arrayIndex)    {        if(array == null)            throw new ArgumentNullException(nameof(array));        if(arrayIndex < 0)            throw new ArgumentOutOfRangeException(nameof(arrayIndex));        for(int i = 0; i < _handles.Count;)        {            GCHandle handle = _handles[i];            object target = handle.Target;            if(target == null)            {                handle.Free();                _handles.RemoveAt(i);            }            else            {                if(arrayIndex >= array.Length)                    throw new ArgumentOutOfRangeException(nameof(arrayIndex));                array[arrayIndex++] = (T)target;                i++;            }        }        _countAfterLastPurge = _handles.Count;        GC.KeepAlive(this);    }    /// <summary>    /// Removes any memory allocated for tracking objects that have been collected and are no longer alive.    /// </summary>    public void Purge()    {        for(int i = 0; i < _handles.Count;)        {            GCHandle handle = _handles[i];            if(handle.Target == null)            {                handle.Free();                _handles.RemoveAt(i);            }            else            {                i++;            }        }        _countAfterLastPurge = _handles.Count;        GC.KeepAlive(this);    }    int ICollection<T>.Count => throw new NotSupportedException(The exact number of living objects in a WeakCollection can never be ascertained. Call GetCount() to get an approximate count of objects.);    bool ICollection<T>.IsReadOnly => false;}
WeakCollection class
c#;.net;memory management;generics;collections