The "Weak Event" Pattern in C#

27/08/2023
C#.NET

Events in C# are a powerful mechanism for decoupling components and enabling a publisher/subscriber model. However, they have a significant drawback: the publisher holds a strong reference to the subscriber, and this can cause memory leaks. This article describes a pattern for implementing weak events in C#.

The Problem with Events and Memory Management

In a typical event-based system, a publisher exposes an event, and subscribers register to receive notifications when the event is raised. The publisher holds a reference to the subscriber, and when the event is raised, the publisher calls the subscriber's event handler. The issue arises when a subscriber forgets to unregister from the event. If the publisher holds a strong reference to the subscriber through this event, then even if other parts of the application have released their references to the subscriber, the subscriber won't be garbage collected. This creates a memory leak. And this also goes the other way around: Imagine a long-living publisher and a short-living subscriber. If the subscriber forgets to unregister from the event, the publisher will hold a reference to the subscriber, and the subscriber won't be garbage collected!

A small detour to the Garbage Collector

I already talked about the Garbage Collector and when objects get removed in my blog post: "The garbage collector in .NET". The essence is that in the marking phase, the garbage collector checks for non-reachable objects. So everything that is connected to a root object and has a direct reference to another object, then this object is "alive" and will not be cleaned up.

GC

But the picture shifts with "Weak References" as they are not "seen" in the same way by the Garbage Collector. So we give the opportunity to clean up those resources, once they are no longer used.

The Solution: Weak Events and Weak References

The core idea of the solution is to use a weak reference to the subscriber instead of a strong reference. A weak reference is a reference that doesn't prevent the object it refers to from being garbage collected. The .NET framework provides the WeakReference class for this purpose. The WeakReference class has a Target property that returns the object it refers to, or null if the object has been garbage collected. The WeakReference class also has an IsAlive property that returns true if the object has not been garbage collected yet. A weak reference allows the garbage collector to reclaim an object while still allowing it to be resurrected if it’s accessed again. In simple terms, it doesn't prevent the garbage collector from collecting its target.

Simple Implementation using Weak Event Pattern in C#

The following code shows a simple implementation of the weak event pattern in C#:

public class WeakEvent<TEventArgs>
{
    private readonly List<WeakReference> _listeners = new List<WeakReference>();

    public void AddListener(EventHandler<TEventArgs> handler)
    {
        _listeners.Add(new WeakReference(handler));
    }

    public void RemoveListener(EventHandler<TEventArgs> handler)
    {
        _listeners.RemoveAll(wr => !wr.IsAlive || wr.Target.Equals(handler));
    }

    public void Raise(object sender, TEventArgs args)
    {
        for (int i = _listeners.Count - 1; i >= 0; i--)
        {
            var weakReference = _listeners[i];
            if (weakReference.IsAlive)
            {
                ((EventHandler<TEventArgs>)weakReference.Target)?.Invoke(sender, args);
            }
            else
            {
                _listeners.RemoveAt(i);
            }
        }
    }
}

The WeakEvent class has a list of WeakReference objects. The AddListener method adds a new listener to the list. The RemoveListener method removes a listener from the list. The Raise method raises the event by calling the event handler of each listener in the list. The Raise method also removes any dead listeners from the list.

Using the Weak Event Pattern in C#

The following code shows how to use the WeakEvent class:

public class Publisher
{
    private readonly WeakEvent<EventArgs> _event = new WeakEvent<EventArgs>();

    public event EventHandler<EventArgs> Event
    {
        add => _event.AddListener(value);
        remove => _event.RemoveListener(value);
    }

    public void RaiseEvent()
    {
        _event.Raise(this, EventArgs.Empty);
    }
}

What is the difference between this implementation and a standard event? The difference is that the WeakEvent class doesn't hold a strong reference to the subscriber under the hood.

Conclusion

The "weak event" pattern is one way of avoiding memory leaks in event-based systems. THere is even a proposal on the official runtime repository making this pattern first class citizen with better language and runtime support. Obviously the implemetation above is super simple and doesn't cover all edge cases (or any at all).

18
An error has occurred. This application may no longer respond until reloaded. Reload x