How to Unsubscribe from C# Events: Syntax and Memory Leak Prevention
Learn how to use -= to unsubscribe from C# events, avoid memory leaks, and manage event handler lifetimes with practical examples.
In C#, events are built on delegates. Subscribing with += adds a method reference to the publisher's invocation list, and unsubscribing with -= removes that reference. If a publisher keeps a strong reference to a subscriber that is no longer needed, the subscriber cannot be garbage collected. This article explains the -= syntax, the common pitfalls, and practical patterns for managing event handler lifetimes.
Why Unsubscribing Matters
Every += subscription creates a strong reference from the publisher to the subscriber. As long as the publisher is alive, the subscriber cannot be collected, even if the subscriber is no longer needed. This is a common source of memory leaks in desktop, web, and service applications.
Consider a UI window that subscribes to a background service's event. If the window is closed but the service still holds a reference to the window's handler, the window remains in memory. Over time, repeated open/close cycles accumulate unreachable objects, increasing memory pressure and degrading performance.
Unsubscribing with -= breaks that reference, allowing the subscriber to be garbage collected when no other references exist.
Basic Syntax: Using -= to Unsubscribe
The -= operator removes a handler from an event. The compiler translates it to a call to the event's remove accessor, which removes the delegate from the invocation list.
public class Publisher { public event EventHandler? SomethingHappened; } public class Subscriber { public void HandleEvent(object? sender, EventArgs e) { Console.WriteLine("Handled"); } } // Usage var publisher = new Publisher(); var subscriber = new Subscriber(); publisher.SomethingHappened += subscriber.HandleEvent; // ... later publisher.SomethingHappened -= subscriber.HandleEvent;
The handler must match the delegate signature. For EventHandler, the method must accept object? sender and EventArgs e. If you subscribe with a lambda or anonymous method, keep a reference to the delegate so you can remove it later.
Unsubscribing from Anonymous Methods and Lambdas
Subscribing with a lambda and then attempting to unsubscribe by writing the same lambda expression compiles, but it is not guaranteed to refer to the same delegate. If the lambda captures variables, it creates a new delegate instance each time. Store the delegate in a variable instead.
EventHandler handler = (sender, e) => Console.WriteLine("Handled"); publisher.SomethingHappened += handler; // ... later publisher.SomethingHappened -= handler;
If you need to unsubscribe inside the handler itself, you can capture the delegate variable. Assign the variable before subscribing so the lambda can refer to it when the event is raised.
EventHandler? handler = null; handler = (sender, e) => { Console.WriteLine("Handled"); publisher.SomethingHappened -= handler; }; publisher.SomethingHappened += handler;
This pattern is useful for one-time event handling.
Handling Multiple Subscriptions and Null References
An event can have multiple subscribers. Unsubscribing with -= removes the last matching delegate in the invocation list. If the same method is subscribed multiple times, each += adds a separate entry, and each -= removes one occurrence.
publisher.SomethingHappened += subscriber.HandleEvent; publisher.SomethingHappened += subscriber.HandleEvent; // Invocation list now has two entries. publisher.SomethingHappened -= subscriber.HandleEvent; // Removes one entry; the handler is still subscribed.
If you attempt to unsubscribe a handler that was never added, the operation is a no-op and does not throw. This is safe, but it can mask logic errors if you expect the handler to be present.
For custom events, you can implement explicit add and remove accessors to control the underlying storage. The default event field uses a delegate field that is null when no subscribers exist. Raising an event safely uses the ?.Invoke pattern.
Memory Leaks: When Unsubscription Is Critical
Memory leaks occur when a publisher outlives the subscriber and the subscriber is no longer needed. Common scenarios include:
- Static events: A static publisher lives for the entire process, so any subscriber is retained forever unless unsubscribed.
- Long-lived services: A service that raises events and holds subscribers can keep UI components or temporary objects alive.
- Timer events: Subscribing to
System.Timers.Timer.Elapsedwithout unsubscribing keeps the timer's target alive.
In these cases, unsubscribing is required for correct memory behavior. The cost of a missed -= is not immediate but accumulates over time.
Weak Event Patterns for Long-Lived Publishers
When a publisher is long-lived and subscribers are short-lived, manual unsubscription is error-prone. WPF includes WeakEventManager for this scenario. A minimal weak event can store a weak reference to the subscriber target and the method, then create the delegate only when the event is raised.
Storing a WeakReference to the delegate itself is not enough, because the delegate strongly references the subscriber. The example below stores a weak reference to the target and creates a fresh delegate on demand.
using System.Reflection; public class WeakEventPublisher { private readonly List<(WeakReference Target, MethodInfo Method)> _handlers = new(); public event EventHandler? SomethingHappened { add { if (value.Target is null) throw new NotSupportedException("Static handlers are not supported by this example."); _handlers.Add((new WeakReference(value.Target), value.Method)); } remove { int index = _handlers.FindIndex(h => h.Target.Target == value.Target && h.Method == value.Method); if (index >= 0) _handlers.RemoveAt(index); } } protected void RaiseEvent() { foreach (var handler in _handlers.ToArray()) { if (handler.Target.Target is { } subscriber) { var d = (EventHandler)Delegate.CreateDelegate(typeof(EventHandler), subscriber, handler.Method); d(this, EventArgs.Empty); } else { _handlers.Remove(handler); } } } }
This implementation avoids holding a strong reference to the subscriber, because it stores the method and a weak reference to the target. It removes dead entries while raising. It also adds complexity, does not handle static handlers, and gives no cleanup guarantee. Use a framework-provided weak event manager when possible.
Lifecycle Management: When to Unsubscribe
Decide where to unsubscribe based on the subscriber's lifecycle. For UI elements, unsubscribe in the Dispose method or the Closed event. For services, unsubscribe when the consumer is no longer active.
public class Subscriber : IDisposable { private readonly Publisher _publisher; public Subscriber(Publisher publisher) { _publisher = publisher; _publisher.SomethingHappened += HandleEvent; } public void Dispose() { _publisher.SomethingHappened -= HandleEvent; } private void HandleEvent(object? sender, EventArgs e) { } }
Implementing IDisposable makes the unsubscription explicit and testable. In async or short-lived scopes, consider using try/finally to guarantee removal even if an exception occurs.
Common Mistakes and Edge Cases
One frequent mistake is subscribing with a lambda and attempting to unsubscribe by writing the same lambda text. The code compiles, but the second lambda is not guaranteed to be the same delegate instance, so the original handler may remain subscribed. Use a stored delegate variable instead.
Another mistake is unsubscribing before the handler is added, which silently does nothing.
When an event is raised on a different thread, unsubscription can race with invocation. The default field-like event accessors coordinate concurrent += and -= calls, but invocation still needs a snapshot of the delegate to avoid calling a handler that was removed. If your event needs stronger serialization, implement custom accessors with a lock or use Interlocked operations on the backing delegate field.
private EventHandler? _somethingHappened; private readonly object _lock = new(); public event EventHandler? SomethingHappened { add { lock (_lock) { _somethingHappened += value; } } remove { lock (_lock) { _somethingHappened -= value; } } }
This ensures concurrent subscriptions and unsubscriptions do not corrupt the delegate chain, though the invocation itself still needs a snapshot to avoid races.
Finally, remember that unsubscription does not throw if the handler is not present. If you rely on a handler being removed, verify the subscription logic rather than assuming -= will fail loudly. Using a debug assertion or a custom event accessor that tracks subscriptions can help catch logic errors early.