How Events Encapsulate Delegates in C
In C#, events provide a specialized mechanism to implement the
publisher-subscriber pattern securely by wrapping underlying delegates.
While standard delegates offer flexibility by treating methods as
first-class objects, exposing them publicly creates critical
vulnerabilities such as unauthorized external invocation and accidental
handler overwriting. This article explains how the event
keyword encapsulates delegate instances by generating restricted
accessors, enforcing compile-time boundaries, and ensuring that only the
defining class can trigger the event.
The Problem with Public Delegates
When a delegate is declared as a public field, any external class with access to the object can interact with it without restriction. Consider a public delegate field:
public class Downloader
{
public Action<int> ProgressChanged;
}Exposing a raw delegate like this presents two major problems:
Direct External Invocation: Any external code can call
downloader.ProgressChanged(100)directly, broadcasting a false state change even if no download occurred.Destructive Overwriting: An external subscriber can accidentally use the assignment operator (
=) instead of the addition assignment operator (+=):downloader.ProgressChanged = MyHandler; // Overwrites all other subscribed listenersFurthermore, any external caller can completely clear the invocation list by setting
downloader.ProgressChanged = null;.
How the
event Keyword Solves the Issue
Declaring a delegate with the event keyword introduces
an abstraction layer similar to how properties encapsulate private
fields.
public class Downloader
{
public event Action<int> ProgressChanged;
}When the C# compiler encounters an event declaration, it does not expose the delegate field directly. Instead, it performs two critical operations behind the scenes:
- Generates a Private Delegate Field: The compiler
creates a hidden, private delegate backing field (typically named
something like
ProgressChangedinternally). Because the field is private, it cannot be read, invoked, or directly assigned outside of the declaring class. - Generates Public Accessor Methods: The compiler
creates two public accessor methods:
add_ProgressChangedandremove_ProgressChanged. These methods manage the invocation list using thread-safe operations (Delegate.CombineandDelegate.Remove).
Compile-Time Enforcement
The presence of the event keyword changes how the
compiler parses access to the member from external code:
- Restricted Operators: Outside the declaring class,
only the
+=and-=operators are permitted. The compiler maps+=to the generatedaddaccessor and-=to theremoveaccessor. - Prohibited Invocation: Attempting to invoke the
event from outside the declaring class using
downloader.ProgressChanged(50)results in a compile-time error:CS0070: The event 'Downloader.ProgressChanged' can only appear on the left hand side of += or -= (except when used from within the type 'Downloader'). - Prohibited Direct Assignment: Attempting to assign
downloader.ProgressChanged = MyHandleror set it tonullalso triggers a compile-time error.
Raising the Event Safely
Because external classes can only subscribe or unsubscribe, the publishing class retains exclusive ownership over when and how the delegate is invoked. The publishing class typically triggers the event using a null-conditional check:
public class Downloader
{
public event Action<int> ProgressChanged;
public void StartDownload()
{
// Safe invocation: only the declaring class can invoke the delegate
ProgressChanged?.Invoke(50);
}
}By restricting direct access to the delegate's invocation list and
restricting external consumers to add and
remove operations, C# events effectively eliminate state
interference and protect the integrity of the publisher-subscriber
relationship.