Nikos Ioannou

Feb 5, 2021

2 min read

Weak references in C# (and Unity)

Weak references allow loading resources and letting the garbage collector (GC) destroy them at will to free memory. This simplifies memory management for objects that only need to be used temporarily, and it’s difficult or impossible to explicitly manage their lifecycle. Note that another solution in C# is a MemoryCache that you should look into if you are considering weak references.

Let’s start with a bug! Assume we have a class named Fooer with a method called Foo(). Fooer consumes a lot of memory, and we need a wrapper class to allow the GC collect it.

// DOES NOT WORK!!class FooerHolder {
private readonly WeakReference fooRef;
public FooHolder() {
fooRef = new WeakReference(null);
}

public void Foo() {
Fooer fooer;
// Doesn’t matter if we check either or both these conditions.
if (objectRef.IsAlive && objectRef.Target != null) {
// uh-oh GC might have done its magic, after we checked.
fooer = fooRef.Target as Fooer;
} else {
fooer = Fooer.Load();
fooRef.SetTarget(fooer);
}
// Boom! NPE — sometimes…
fooer.Foo();
}
}

This code will occasionally fail, creating a bug that’s tough to pin down. This is especially surprising in Unity, where you expect to be operating in a single thread. Even in Unity, though, the garbage collector runs in a separate thread, so between the weak reference validity check and the assignment, fooRef.Target could have been set to null by the GC.

The correct way to do this is by first assigning the target object to a strong reference and then checking if that reference is null.

class FooerHolder {
private readonly WeakReference fooerRef;
public FooerHolder() {
fooerRef = new WeakReference(null);
}

public void Foo() {
// Create a strong reference.
Fooer fooer = fooerRef.Target as Fooer;
// Check if the strong reference points to anything.
if (fooer == null) {
fooer = Fooer.Load();
fooerRef.SetTarget(fooer);
}
// "fooer" is a strong reference, GC cannot dispose the object
// it references. If Fooer.loadObject() never returns null,
// we are safe here.
fooer.Foo();
}
}

If the object referred to by the weak reference is still in memory, a strong reference is created to it in the stack. This prevents the garbage collector from destroying it until that (or any other) hard reference is removed. When the Foo method returns, any references created in the stack, including our hard reference, will be removed. Therefore after exiting the method, the GC can safely destroy the object.

This also shows that using the WeakReference.IsAlive method is pointless, as between checking and getting the referred object, the GC might have collected it.

Happy weak referencing!