CodeGym /Courses /C# SELF /Unsubscribing from events ( -...

Unsubscribing from events ( -=) and memory leaks

C# SELF
Level 53 , Lesson 1
Available

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.

2
Task
C# SELF, level 53, lesson 1
Locked
Subscribing and Unsubscribing from Events
Subscribing and Unsubscribing from Events
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION