On weak references and how to test them
Table of Contents
Introduction #
When working in C#, most of the time we access objects through so-called strong references. With strong references, if the application code can still reach a certain object, then it cannot be garbage collected and, if that object is then referencing other objects, then those also cannot be collected themselves.
Weak references, instead, allow the garbage collector to freely collect objects at will to free memory space, while still allowing to access them before they get collected. Weak references are particularly useful when we need to reference objects that can potentially use a lot of memory, but can be recreated easily. Weak references can also be useful to avoid creating strong circular references that can prevent objects from being collected, for instance in iOS.
In this small blog post I’ll go over how to create and use a weak reference, and also how to test that it gets correctly garbage collected. If you need more information about weak references themselves, then you can take a look at the documentation.
How to create and use a weak reference #
In C# you can use the WeakReference
class to create a weak reference to an object.
For example, let’s imagine that we have a Container
class with a MainView
property. MainView
could potentially occupy quite a lot of memory, but it’s relatively easy to re-create. For this reason we prefer to keep it as a weak reference:
public class Container
{
private WeakReference<View>? mainViewWeakReference;
public View MainView
{
get
{
if (mainViewWeakReference is null || !mainViewWeakReference.TryGetTarget(out var targetView))
{
// Possibly View could occupy a lot of memory
targetView = new View();
if (mainViewWeakReference is null)
{
mainViewWeakReference = new WeakReference<View>(targetView);
}
else
{
mainViewWeakReference.SetTarget(targetView);
}
}
return targetView;
}
}
}
The MainView
property here is used to return the contained main view, as an example.
There are a couple of things to notice:
- We use
TryGetTarget
to check if the containedView
has been collected already. If it has not been collected, then we just return it. - Otherwise we create the
View
object, and then we either create theWeakReference<View>
object, or we change the target object contained inviewWeakReference
withSetTarget
.
After the getter is called, the GC is free to collect the contained view when there are not other strong references to it.
Another small thing to notice is that we actually have a strong reference to WeakReference
in the code, but the View
object inside it is stored as a weak reference.
How to test a weak reference #
Writing a unit test to verify if the target of a weak reference is garbage collected correctly is a little bit tricky, because in order for that to happen there shouldn’t be any strong reference to it. For this reason, the easiest way to create a unit test is to make a weak reference to the object that we expect to be collected in a delegate or another method, for example in an Action
or Func
, like in the following example:
[Test]
public void VerifyThatViewIsGarbageCollected()
{
var container = new Container();
var wr = new Func<WeakReference>(() =>
{
return new WeakReference(container.MainView);
})();
GC.Collect();
Assert.That(wr.IsAlive, Is.False);
}
This test is verifying that Container.MainView
is garbage collected. To do so, we create a weak reference to it in the Func
, and then return it. We then force garbage collection by calling GC.Collect
, and then we verify that the target has been garbage collected with IsAlive
. In this case we are creating the weak reference inside the Func
delegate, so that after the delegate is called, there will be no actual strong reference to MainView
, so that we can verify it gets actually collected.
As a final note, please remember that the way that garbage collection works is slightly different ways when working in Debug or Release mode, so that’s something to take into considerations when testing.