This article explains how to create a weak list (a list that allows items to be collected by the GC).
I already wrote an article about WeakArrays, but the problem of weak arrays is that they have fixed length. Now, I will explain how to create a weak list.
First, how can we create a weak list?
Solution 1: In our class, we create a list of
WeakReferences, but we implement all methods to get and set values of the data type, so the weak references are not seen. This one has the problem that each
WeakReference has a destructor.
Solution 2: In our class, we create a list of
GCHandles. We also implement all methods to get and set values of the data type, so the
GCHandles are not seen. We must then implement the finalizer to free all
GCHandles at once.
Solution 3: In our class, we create an
GCHandles, and control when such array must be resized, in a process similar to the original list. We must also implement the finalizer.
I must say that all of the solutions work but the first one is the slowest and the last one is the one that allows better control of everything. But, I will try to explain only the last one, which is the approach I use.
So, the things needed to write a weak list are:
- How a normal list works
- How to make the "weak" part work
- And how to make it thread-safe. Even if the list is theoretically only used by one Thread, the GC always runs as another thread at unexpected times.
The Normal List
The normal list, in practice, is very simple. It contains an array (in my case, I initialize it with length 32) and a
Count. Every time an item is added, it tries to put the item into the actual count and then increase the count. Of course, if there are more items than count, then a new array is created, with double the size, and the item is put at the
Count position and
Count is incremented. Of course, there are a lot of methods, like
Remove and others, but they find items sequentially and, if one item is removed, all items are copied to one position before, and then the
Count is decremented. Simple, isn't it?
Now, how do we make it weak?
As I said earlier, I will use
GCHandles. So, instead of having a
Button (if we have an
WeakList<button>) or a
string (if we have a
WeakList<string>), we will always have a
GCHandle. Then, to get a value at a given index, we will have a code that follows this pattern:
public T this[int index]
But, two important things:
First, this "pattern" is not yet thread-safe, and it must be. Second, the
Count will be a fake value. It will only tell how many Items where added that we do not know to have been collected. So, a list with
count 5 can, in fact, have 0 real items because all of them where collected.
And, finally, we must make it thread-safe.
The simplest way to make things thread-safe is to use Locks. And that's what I do. I don't use
ReaderWriterLocks because they are slower than custom locks, and their benefit is only useful when the write operations are really slow, which is not the case. So, each method should lock the object (or better, some internal synchronization object), check if the object was not disposed, and then do its real work.
Ok... I said finally, but there are 2 more steps:
IDispose. This is only needed to follow the guideline that, instead of counting on the
GarbageCollector to finalize our object, if we know it will not be used anymore, we can free it immediately, and so the entire object will be collected in the next collection (if we run the finalizer, the object will only be collected one generation after that in which its finalizer was run).
- Know when items were collected, so all the
GCHandles that are now
null can be removed.
To do this, we can constantly check the number of collection counts (at each insert method, for example), we can use a secondary thread with a timer, or we can discover when a collection was done to call an event. I already created a class (
GCUtils) which has the
Collected event. This is almost a hack, as I create an object without strong references (so it is always collected) and, in its finalize, I re-register it for finalize (so it does not die) and call set a
ManualResetEvent object. So, as soon the GC terminates and all threads start to run again, my thread that is waiting on such
ManualResetEvent calls the
Collected event. Such event has the advantage that, if collections happen frequently, then the extra items will be removed frequently. And, if collections take long time to happen, then there will not be a process trying to remove them for nothing. But any method will work. So, the object must be locked, the number of non-collected items must be counted, then a new array must be created with, at least, the number of non-collected items and, finally, all valid handles must be copied and
null handles must be freed. Note that between the count and the copy to the new array, more items can be collected, so the count must only be set after such copy is done.
And, that's finally it.
Now I will show the code for some key-methods:
public WeakList(int initialCapacity)
if (initialCapacity < 1)
throw new ArgumentOutOfRangeException
("initialCapacity", "The initial accepted capacity value is 1.");
fHandles = new GCHandle[initialCapacity];
GCUtils.Collected += p_Collected;
protected override void Dispose(bool disposing)
GCUtils.Collected -= p_Collected;
var handles = fHandles;
if (handles != null)
int count = handles.Length;
for (int i=0; i<count; i++)
var handle = handles[i];
fHandles = null;
public void Add(T item)
if (item == null)
throw new ArgumentNullException("item");
int count = Count;
if (count == fHandles.Length)
Array.Resize(ref fHandles, count * 2);
fHandles[count] = GCHandle.Alloc(item, GCHandleType.Weak);
To see the full implementation, download the library and look at Caching\WeakList.cs.
Well, I don't know when I will publish the other articles, but I plan to explain how a
WeakHashSet and a
WeakDictionary work, as I think both are more useful than weak-lists.
- 26th March, 2010: Initial version