What is a Delegate?
A delegate in C# is a type-safe reference type that holds references to methods with a specific signature. Unlike function pointers in C++, which only point to functions, delegates encapsulate both an object instance and a method. This makes delegates fully object-oriented and type-safe.
When you declare a delegate, you are essentially defining a new class that derives from System.Delegate. The declaration follows this pattern:
[attributes] [modifiers] delegate return_type DelegateName(parameters);
For instance, delegate void NotifyHandler(string message); defines a delegate type that can reference any method returning void and accepting a string parameter.
How Delegates Work
Think of a delegate as a data type similar to classes like string or custom classes. Just as you declare string name or Person person, you can declare delegate-typed variables. However, instead of referencing objects, these variables reference methods.
Consider the following delegate and class definitions:
using System;
namespace DelegateDemo
{
public delegate void MessageHandler(string message);
public class NotificationService
{
public string ServiceName { get; set; }
public MessageHandler MessageProcessor { get; set; }
public NotificationService(string name)
{
ServiceName = name;
}
public void ProcessMessage(string msg)
{
if (MessageProcessor != null)
{
MessageProcessor(msg);
}
}
}
}
The usage demonstrates how different objects can have different behaviors assigned to the same delegate-typed property:
using System;
namespace DelegateDemo
{
class Program
{
static void Main(string[] args)
{
NotificationService emailService = new NotificationService("Email");
emailService.MessageProcessor = new MessageHandler(ProcessEmail);
emailService.ProcessMessage("Hello World");
Console.WriteLine("--------------------------------------");
NotificationService smsService = new NotificationService("SMS");
smsService.MessageProcessor = ProcessSms;
smsService.ProcessMessage("Hello World");
Console.ReadLine();
}
static void ProcessEmail(string message)
{
Console.WriteLine($"Email sent: {message}");
}
static void ProcessSms(string message)
{
Console.WriteLine($"SMS sent: {message}");
}
}
}
Delegate Invocation List
One powerful feature of delegates is their ability to hold references to multiple methods. The collection of methods is called the invocation list. You can use + and += to add methods, - and -= to remove methods.
using System;
namespace DelegateDemo
{
class Program
{
static void Main(string[] args)
{
NotificationService emailService = new NotificationService("Email");
emailService.MessageProcessor = new MessageHandler(ProcessEmail);
emailService.MessageProcessor += LogMessage;
emailService.ProcessMessage("Important Update");
Console.WriteLine("--------------------------------------");
NotificationService smsService = new NotificationService("SMS");
smsService.MessageProcessor = ProcessSms;
smsService.MessageProcessor += LogMessage;
smsService.ProcessMessage("Important Update");
Console.ReadLine();
}
static void ProcessEmail(string message)
{
Console.WriteLine($"Email sent: {message}");
}
static void ProcessSms(string message)
{
Console.WriteLine($"SMS sent: {message}");
}
static void LogMessage(string message)
{
Console.WriteLine($"Log: Processing message - {message}");
}
}
}
When ProcessMessage is called, both methods in the invocation list execute sequentially.
Why Use Delegates?
Delegates enable loose coupling and flexible design patterns. Without delegates, you might need conditional logic to determine which behavior to execute:
// Without delegates - poor extensibility
if (serviceType == "Email")
SendEmail();
else if (serviceType == "SMS")
SendSms();
This approach requires modifying code whenever new service types are added. With delegates, you simply assign different methods at runtime, making the code more maintainable and extensible.
An alternative design uses inheritance and polymorphism with virtual methods:
public abstract class NotificationService
{
public abstract void Send(string message);
}
public class EmailService : NotificationService
{
public override void Send(string message) { /* email logic */ }
}
While this works, delegates are more lightweight when only behavior differs without needing a full class hierarchy.
Events in C#
Events provide a mechanism for objects to communicate through messaging. The object raising the event is the publisher, while objects responding are subscribers. The publisher doesn't need to know which objects will handle the event—this connection is established through delegates.
A event declaration follows this pattern:
[attributes] [modifiers] event delegate_type event_name;
Events encapsulate delegate variables following object-oriented principles. Just as properties encapsulate fields, events encapsulate delegates. This prevents external code from directly assigning to the delegate (which would overwrite all subscribers) while still allowing subscription and unsubscription.
Here is how events are declared and used:
using System;
namespace EventDemo
{
public delegate void AlertDelegate(string alert);
public class MonitorSystem
{
public string MonitorName { get; set; }
// Event declaration instead of public delegate field
public event AlertDelegate AlertHandler;
public MonitorSystem(string name)
{
MonitorName = name;
}
public void TriggerAlert(string message)
{
if (AlertHandler != null)
{
AlertHandler(message);
}
}
}
}
using System;
namespace EventDemo
{
class Program
{
static void Main(string[] args)
{
MonitorSystem serverMonitor = new MonitorSystem("Server-01");
serverMonitor.AlertHandler += new AlertDelegate(LogAlert);
serverMonitor.AlertHandler += EmailNotification;
serverMonitor.TriggerAlert("CPU usage high");
Console.WriteLine("--------------------------------------");
MonitorSystem networkMonitor = new MonitorSystem("Network-Switch");
networkMonitor.AlertHandler += new AlertDelegate(SlackNotification);
networkMonitor.AlertHandler += LogAlert;
networkMonitor.TriggerAlert("Connection lost");
Console.ReadLine();
}
static void LogAlert(string message)
{
Console.WriteLine($"Logged: {message}");
}
static void EmailNotification(string message)
{
Console.WriteLine($"Email notification: {message}");
}
static void SlackNotification(string message)
{
Console.WriteLine($"Slack notification: {message}");
}
}
}
Note the key difference: with raw delegates, you can use = to assign a single method. With events, you must use += to subscribe and -= to unsubscribe, preventing accidental overwriting of all subscribers.
Delegates vs the Observer Pattern
Both delegates and the Observer design pattern facilitate communication between objects. In the Observer pattern, the subject maintains references to observer objects and calls their update methods. Delegates achieve similar functionality but at the method level rather than the class level.
With delegates, an object directly references method pointers. With the Observer pattern, you would typically have a subject class containing a collection of observer objects, then call methods on those observers. Delegates provide a more direct, type-safe mechanism for one-to-many notifications without requiring separate observer interface definitions.