1. Introduction
In a way, subscribing to an event in C# is like subscribing to a friend's meme newsletter: updates keep coming until you say "enough" and unsubscribe. In programming this is especially important because a forgotten subscription is not just "one more meme", it's a memory leak!
Imagine you have a form in an app (for example, a settings dialog). It subscribes to the main window's event to react to changes. The user closes the form thinking it's destroyed, but the handler is still subscribed! The form is still alive in memory because the main object holds a reference to it via the event.
Conclusion: If a subscriber object subscribes to a publisher's event and "forgets" to unsubscribe, the garbage collector won't collect it while the publisher is alive.
Let's review the += operator and show -=
- += — subscribe: adds a handler to the event invocation list.
- -= — unsubscribe: removes a handler from the invocation list.
This looks roughly like this:
worker.WorkCompleted += handler; // subscribe
worker.WorkCompleted -= handler; // unsubscribe
If a handler was added twice, you need to remove it the same number of times for it to actually disappear from the invocation list.
A bit of internals
Under the hood an event in C# is a delegate field (or a list of delegates), and the += operator actually calls Delegate.Combine, while -= calls Delegate.Remove. The object that subscribed to the event becomes part of the reference graph. That's why a forgotten subscription = memory leak.
2. Memory leaks via events: how it works
Classic scenario
class Window
{
public event EventHandler Updated;
public void SimulateUpdate()
{
// Simulation: notify all subscribers
Updated?.Invoke(this, EventArgs.Empty);
}
}
class SettingsForm
{
public void OnWindowUpdated(object sender, EventArgs e)
{
Console.WriteLine("SettingsForm reacts to window update");
}
}
Step by step:
var window = new Window();
var settingsForm = new SettingsForm();
window.Updated += settingsForm.OnWindowUpdated;
window.SimulateUpdate(); // SettingsForm reacts
// The user closed the form. We lose all references to it:
settingsForm = null;
// But the SettingsForm object will NOT be collected by GC while window is alive,
// because window.Updated still holds a reference to the OnWindowUpdated method,
// and therefore to the SettingsForm instance.
What to do?
Unsubscribe:
// For this you need to keep a reference to the handler or the object:
window.Updated -= settingsForm.OnWindowUpdated;
settingsForm = null; // Now the object can be collected
Table: who holds a reference to whom
| Action | Who holds the reference | Can memory be freed? |
|---|---|---|
| Subscribe to event (+=) | Publisher to subscriber | No, while the publisher is alive |
| Unsubscribe (-=) | No | Yes, after removing all external references |
| No subscription | No | Yes |
3. How to organize unsubscription correctly
Explicit handler removal
You can do this, for example, when a window or form is closed:
class SettingsForm
{
private readonly Window _window;
public SettingsForm(Window window)
{
_window = window;
_window.Updated += OnWindowUpdated;
}
public void Close()
{
_window.Updated -= OnWindowUpdated; // unsubscribe!
// here goes code to close (e.g. Dispose, GC.SuppressFinalize, etc.)
}
public void OnWindowUpdated(object sender, EventArgs e)
{
// Event handling
}
}
If SettingsForm is destroyed via a "close" button, it's important not to forget to call the method that performs the unsubscription (for example, Close()).
Using the IDisposable interface
For complex objects that subscribe to events and control their lifecycle, it's convenient to implement IDisposable. In the Dispose() method you perform all needed unsubscriptions.
class SettingsForm : IDisposable
{
private readonly Window _window;
public SettingsForm(Window window)
{
_window = window;
_window.Updated += OnWindowUpdated;
}
public void OnWindowUpdated(object sender, EventArgs e)
{
// ...
}
public void Dispose()
{
_window.Updated -= OnWindowUpdated;
// Also free other resources here
}
}
Now SettingsForm can be used inside a using block, you can call Dispose() explicitly, or automate resource release (for example, via GC.SuppressFinalize for finalizable types).
4. Working with lambda expressions: dangers and tricks
If you subscribe to an event using a lambda expression but don't store the lambda in a variable, you won't be able to unsubscribe!
// Subscription — anonymous lambda
window.Updated += (s, e) => Console.WriteLine("Lambda called!");
// How to unsubscribe now? — Can't!
window.Updated -= (s, e) => Console.WriteLine("Lambda called!"); // This is a different delegate!
What to do?
Store the lambda in a delegate variable:
EventHandler handler = (s, e) => Console.WriteLine("Lambda called!");
window.Updated += handler;
// ... now you can unsubscribe!
window.Updated -= handler;
5. Useful nuances
Lifecycle peculiarities of objects and events
Another common problem is cross-references via events between two long-lived objects. For example, one window subscribes to another's event, both are actively used and not collected — memory usage keeps growing.
Recommendation: Always keep track of who subscribes to whom and when to unsubscribe. If the subscription has the same lifecycle as the publisher — fine. If the subscriber may live shorter than the publisher, implement explicit unsubscription.
Universal rule: "If you subscribed — unsubscribe!"
- For long-living publishers (for example, global singletons, main windows) — always implement unsubscription in subscribers.
- For short-lived objects (for example, one-off notifications or events where the subscriber lives longer than the publisher) — you can relax a bit, but still watch the context.
- Use approaches like WeakEvent (weak events) or special frameworks if you don't want to manage unsubscription manually.
6. Typical mistakes when unsubscribing
Unsuccessful unsubscription: the handler must be the same
It's very important that when unsubscribing you specify the exact same handler that was used to subscribe. Otherwise the unsubscription won't work.
Wrong:
window.Updated += settingsForm.OnWindowUpdated;
// ...
window.Updated -= new SettingsForm().OnWindowUpdated; // Won't work! This is another instance and another delegate!
Correct:
window.Updated -= settingsForm.OnWindowUpdated;
If you used an anonymous lambda without saving a reference to the delegate, you can't unsubscribe from it because that would be another delegate instance:
// Subscribe
window.Updated += (s, e) => Console.WriteLine("Lambda!");
// Attempt to unsubscribe — won't work!
window.Updated -= (s, e) => Console.WriteLine("Lambda!");
"Forgotten" unsubscription
Very often unsubscription is simply forgotten, especially when the subscriber lives longer than the publisher or when the developer doesn't fully understand how events work. As a result subscriber objects stay in memory longer than needed, which leads to memory leaks and performance problems.
GO TO FULL VERSION