Originally published at https://allcoderthings.com/en/article/csharp-delegates-and-events
In C#, delegates and events are the foundation of event-driven programming. A delegate is a type-safe reference that can point to a method. An event allows a class to notify the outside world that “a certain action has occurred.” These mechanisms enable loosely coupled architectures and make it possible to dynamically execute different methods.
What is a Delegate?
A delegate is a type that holds references to methods with a specific signature (parameters and return type). In other words, it allows methods to be treated like variables.
A delegate variable can point to a compatible method and invoke it dynamically. This enables flexible and loosely coupled code.
// A delegate definition
public delegate void Notify(string message);
class Program
{
static void SendEmail(string message)
{
Console.WriteLine("Email sent: " + message);
}
static void Main()
{
Notify notifyHandler;
// Assign a method to the delegate
notifyHandler = SendEmail;
notifyHandler("Meeting is at 10:00 AM.");
}
}
In this example, the notifyHandler delegate references the SendEmail method and invokes it.
Multicast Delegates
A multicast delegate is a delegate that can reference multiple methods at the same time. In C#, delegates are multicast by default when you combine them using +=.
When you invoke a multicast delegate, it calls each subscribed method in the invocation list in the order they were added. This is especially useful for broadcasting a notification to multiple listeners.
Example
using System;
class Program
{
delegate void Notify(string message);
static void Main()
{
Notify notifier = LogToConsole;
notifier += LogToFile;
notifier += SendEmail;
notifier("Build completed.");
}
static void LogToConsole(string message)
{
Console.WriteLine("[Console] " + message);
}
static void LogToFile(string message)
{
Console.WriteLine("[File] " + message);
}
static void SendEmail(string message)
{
Console.WriteLine("[Email] " + message);
}
}
You can remove a method from the invocation list using -=:
notifier -= SendEmail;
- Multicast delegates are commonly used behind the scenes in events.
- If one handler throws an exception, invocation stops and the remaining handlers are not executed.
- If the delegate has a return type, only the result of the last method is returned.
Return Value Behavior
If a multicast delegate has a return type, each method in the invocation list is executed, but only the result of the last method is returned.
using System;
class Program
{
delegate int Calculate(int x);
static void Main()
{
Calculate calc = Square;
calc += Double;
int result = calc(5);
Console.WriteLine(result);
// Output: 10 (result of the last method: Double)
}
static int Square(int x)
{
Console.WriteLine("Square called");
return x * x;
}
static int Double(int x)
{
Console.WriteLine("Double called");
return x * 2;
}
}
Even though both Square and Double are executed, the delegate returns the result of Double, because it was the last method added to the invocation list.
- All methods in the invocation list are executed in order.
- Only the return value of the last method is preserved.
- For this reason, multicast delegates are typically used with
voidreturn types (like events).
What is an Event?
An event is a mechanism that signals when an action has occurred. Events are built on top of delegates and provide controlled access to them. The event keyword is used for declaration, and it can only be accessed externally using += (subscribe) or -= (unsubscribe).
This ensures that:
- The event can only be triggered inside the declaring class.
- External code cannot overwrite the invocation list.
- Encapsulation is preserved.
public class Button
{
// Event definition (EventHandler is a standard delegate type)
public event EventHandler? Click;
public void SimulateClick()
{
Console.WriteLine("Button clicked!");
// Trigger the event
Click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var btn = new Button();
// Subscribe to the event
btn.Click += (s, e) => Console.WriteLine("Event: Button was clicked.");
btn.SimulateClick();
}
}
The null-conditional operator (?.) ensures that the event is only invoked if there are subscribers, preventing a NullReferenceException.
Here, the Button class publishes a Click event. The subscribed lambda expression executes when the event is triggered. This pattern is widely used in UI frameworks such as WinForms and WPF.
EventHandler and EventArgs
In C#, the most commonly used standard delegate for events is EventHandler. It follows the conventional event pattern: (object sender, EventArgs e).
By deriving from EventArgs, you can pass additional data related to the event. This follows the official .NET event design guidelines.
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderCreated;
public void CreateOrder(int id, decimal amount)
{
Console.WriteLine($"Order created (ID={id}, Amount={amount})");
// Trigger the event
OrderCreated?.Invoke(this, new OrderEventArgs(id, amount));
}
}
class Program
{
static void Main()
{
var service = new OrderService();
service.OrderCreated += (s, e) =>
{
Console.WriteLine($"Notification: Order received (#{e.OrderId}, {e.Amount} USD)");
};
service.CreateOrder(101, 250m);
}
}
In this example, the OrderService class raises the OrderCreated event whenever a new order is created. The subscriber receives the order details through the custom OrderEventArgs class.
This approach ensures type safety and makes event-driven communication clear and structured.
Delegate vs Event (Encapsulation)
Although events are built on top of delegates, they are not the same thing. The key difference lies in encapsulation and access control.
A delegate field can be modified freely from outside the class, while an event restricts external access and protects the invocation list.
Delegate Example
public class Publisher
{
public Action? OnChange;
}
Because OnChange is a delegate field:
- External code can assign it using
=. - All existing subscribers can be removed accidentally.
- It can even be set to
null. - External code can invoke it directly.
publisher.OnChange = null; // Clears all subscribers
publisher.OnChange = SomeMethod; // Replaces invocation list
publisher.OnChange?.Invoke(); // Can be triggered externally
Event Example
public class Publisher
{
public event Action? OnChange;
}
Because OnChange is declared as an event:
- External code can only use += and -=.
- It cannot overwrite the invocation list using =.
- It cannot invoke the event directly.
- The event can only be triggered inside the declaring class.
- Delegates provide flexibility.
- Events provide controlled access and encapsulation.
- For public notifications, events should almost always be preferred over delegate fields.
Modern Alternatives: Action & Func
In modern C#, defining custom delegate types is often unnecessary. Instead, you can use the built-in generic delegates Action and Func.
These delegates are flexible, concise, and widely used in the .NET ecosystem (including LINQ, async programming, and task-based APIs).
Action
Action represents a method that does not return a value. It can accept zero or more parameters.
Action log = message => Console.WriteLine(message);
log("Application started.");
Func
Func<T, TResult> represents a method that returns a value. The last generic parameter always represents the return type.
Func add = (a, b) => a + b;
int result = add(5, 3); // 8
Actionis used when no return value is needed.Funcis used when a return value is required.- Using these built-in delegates reduces boilerplate code.
- Most modern C# codebases prefer
ActionandFuncover custom delegate definitions.
Unsubscribing & Memory Leaks
One important aspect of working with events is unsubscribing. If you subscribe to an event but never unsubscribe, it may lead to unexpected memory leaks.
Events maintain a strong reference to their subscribers. As long as the publisher object is alive, the subscriber cannot be garbage collected.
Why This Can Be a Problem
Consider a long-lived publisher (for example, a service or singleton) and a short-lived subscriber (for example, a UI component). If the subscriber does not unsubscribe, it will remain in memory even after it is no longer needed.
public class Publisher
{
public event Action? OnChange;
public void Raise()
{
OnChange?.Invoke();
}
}
public class Subscriber
{
private readonly Publisher _publisher;
public Subscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.OnChange += HandleChange;
}
private void HandleChange()
{
Console.WriteLine("Change detected.");
}
public void Dispose()
{
// Important: unsubscribe!
_publisher.OnChange -= HandleChange;
}
}
If Dispose is never called, the Subscriber instance remains referenced by the event and cannot be garbage collected.
- Always unsubscribe from events when the subscriber’s lifetime ends.
- This is especially important in UI frameworks (WinForms, WPF, Blazor, etc.).
- Long-lived publishers + short-lived subscribers = potential memory leak.
- Implementing
IDisposableis a common solution for safe unsubscription.
Thread Safety & Best Practices
When working with events in multi-threaded environments, thread safety becomes important. A common issue occurs when checking for null before invoking an event.
Potential Race Condition
if (OnChange != null)
{
OnChange(this, EventArgs.Empty);
}
Between the null check and the invocation, another thread could unsubscribe from the event. This may result in a NullReferenceException.
Safe Invocation Pattern
To avoid this race condition, copy the delegate reference to a local variable before invoking it:
var handler = OnChange;
handler?.Invoke(this, EventArgs.Empty);
This ensures that the invocation list cannot change between the null check and the method call.
Modern Syntax
In most cases, the null-conditional operator provides a concise and safe way to invoke events:
OnChange?.Invoke(this, EventArgs.Empty);
- Always use safe invocation patterns in multi-threaded scenarios.
- Prefer ?.Invoke for clean and modern syntax.
- Events are multicast delegates — one failing handler can stop execution.
- Consider isolating handler calls with try/catch if reliability is critical.
When to Use Delegates and Events?
-
Delegates: Use when passing methods as parameters, implementing callbacks, or creating flexible execution pipelines. Prefer
ActionandFuncin modern C# code. - Events: Use when exposing notifications from a class while preserving encapsulation. Events are ideal for UI interactions, domain events, and state changes.
- Best Practice: Use events for public notifications and avoid exposing raw delegate fields. Always unsubscribe when necessary and follow safe invocation patterns.
TL;DR
delegate: Type-safe references that point to methods and support multicast invocation.event: A controlled delegate that allows subscription but preserves encapsulation.EventHandlerandEventArgs: Follow the standard .NET event pattern for passing data.ActionandFunc: Modern alternatives to custom delegate definitions.throw;and safe invocation patterns help maintain reliability and debugging clarity.- Always unsubscribe from events when needed to prevent memory leaks.
- Use events for notifications; use delegates (or
Action/Func) for callbacks and execution flow.
Top comments (0)