No Anonymous Delegate Unsubscribe

ID

csharp.no_anonymous_delegate_unsubscribe

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

code_smell, delegate, event, resource-leak

Description

Reports an unsubscription -= whose right-hand side is an anonymous function: a lambda, or a delegate { } anonymous method. Unsubscribing with a method group — Changed -= OnChanged; — is the correct idiom and is not reported.

Rationale

Every anonymous function expression evaluates to a newly created delegate instance. Removal from an invocation list is by delegate equality, so the instance written at the unsubscription site is never the instance that was added at the subscription site: the removal matches nothing and does nothing at all.

The handler therefore stays attached for the whole life of the publisher, and with it everything the closure captured — usually the subscriber itself. The subscriber cannot be collected while the publisher lives, and each subscribe/unsubscribe cycle adds one more handler that will run again the next time the event is raised. Nothing complains, and the line reads exactly like a working unsubscription.

using System;

public class Monitor
{
    private readonly Sensor sensor;

    public void Watch()
    {
        sensor.Changed += (s, e) => Refresh();
    }

    public void Ignore()
    {
        sensor.Changed -= (s, e) => Refresh();          // FLAW, removes nothing
        sensor.Changed -= delegate { Refresh(); };      // FLAW, a second new instance
    }

    public void Detach()
    {
        sensor.Changed -= OnChanged;                    // OK, the same delegate target
    }

    private void OnChanged(object sender, EventArgs e) { Refresh(); }

    private void Refresh() { }
}

Remediation

Keep the handler in a field or a local of delegate type, subscribe with that value, and unsubscribe with the very same value. When the handler needs no captured state, name it as a method and use the method group on both sides, which makes the pairing visible at a glance:

EventHandler handler = (s, e) => Refresh();
sensor.Changed += handler;
// ...
sensor.Changed -= handler;

If a handler genuinely has to unsubscribe itself the first time it runs, capture it in a local before subscribing so the body has a reference to remove.