No Delegate Subtraction
ID |
csharp.no_delegate_subtraction |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
code_smell, delegate, event, suspicious-operator |
Description
Reports - and -= that subtract a composed chain from a delegate — a subtrahend that is
itself several delegates combined with +, rather than a reference to one delegate.
Subtracting a single delegate is well defined and is not reported, whether written as -= or as
-, and however many single subtrahends the chain has. Unsubscribing from an event with
myEvent -= handler is likewise the correct idiom and is not reported.
Rationale
Removal from a delegate chain is defined as removal of a contiguous run, not of individual members. The right-hand chain is searched for inside the left-hand one as a whole sequence; if that exact sequence appears, it is removed, and if it does not, nothing is removed and the result equals the left operand.
So subtracting a two-member chain a + c from a + b + c gives back all three members
untouched, because a and c are not adjacent. Removal also starts from the end of the chain,
which surprises anyone expecting the first match to go. Code that builds a chain dynamically and
then subtracts another chain from it is therefore very likely to keep invoking handlers it
believed were gone.
A single-member subtrahend has none of that ambiguity: the last occurrence of that one delegate is removed, which is exactly what the code reads as.
using System;
public delegate void Notify(string message);
public class Publisher
{
private Notify handlers;
private event EventHandler Changed;
public Notify Prune(Notify all, Notify a, Notify b)
{
return all - (a + b); // FLAW — removes a contiguous run, or nothing at all
}
public void DropChain(Notify a, Notify b)
{
handlers -= a + b; // FLAW — same rule applies to the compound form
}
public void DropOne(Notify one)
{
handlers -= one; // OK, removing a single delegate is well defined
}
public Notify Trim(Notify chain, Notify a, Notify b)
{
return chain - a - b; // OK, each subtrahend is a single delegate
}
public void Unsubscribe(EventHandler handler)
{
Changed -= handler; // OK, the documented way to unsubscribe from an event
}
}
Remediation
Remove the members one at a time, so each removal is the well-defined single-delegate case. When
the set of handlers is genuinely dynamic, keep them in a collection you control — a List<Notify>
— and add or remove entries explicitly, building the chain only when it is time to invoke. For the
single-handler case, prefer an event and its -= accessor, whose semantics are the ones every
reader expects.