Delegates and Events in C#
Delegates and events are fundamental concepts in C# that enable powerful patterns like callback mechanisms, observer patterns, and decoupled communication between objects. They are crucial for building responsive user interfaces and robust applications.
Understanding Delegates
A delegate is a type that represents references to methods with a particular parameter list and return type. Think of it as a type-safe function pointer. Delegates allow you to treat methods as parameters to other methods, or to store them in variables.
Declaring a Delegate
You declare a delegate using the delegate keyword, specifying the return type and parameter types of the methods it can reference.
public delegate void MyDelegate(string message);
Instantiating and Using Delegates
You can instantiate a delegate by assigning a compatible method to it. You can then invoke the delegate, which in turn calls the referenced method(s).
// A method that matches the delegate signature
public void DisplayMessage(string msg)
{
Console.WriteLine($"Message: {msg}");
}
// Inside another method:
MyDelegate del = new MyDelegate(DisplayMessage);
del("Hello, Delegates!");
Multicast Delegates
Delegates can hold references to multiple methods. When you invoke a multicast delegate, all the referenced methods are executed sequentially.
public void LogMessage(string msg)
{
Console.WriteLine($"Log: {msg}");
}
// Combining delegates
MyDelegate multiDel = DisplayMessage;
multiDel += LogMessage; // Add another method
multiDel("This is a multicast message.");
// Removing a method
multiDel -= LogMessage;
Introducing Events
Events are a mechanism that allows a class (the publisher) to notify other classes (subscribers) when something happens. Events are built upon delegates, providing a safe and controlled way for objects to communicate.
Key Components of Events
- Delegate Type: An event is declared using a delegate type. The standard convention for event handler delegates is to return
voidand accept two parameters: anobjectrepresenting the sender, and an object derived fromEventArgscontaining event data. eventKeyword: This keyword is used to declare an event member in a class. It restricts how the delegate can be accessed from outside the class, typically allowing only addition and removal of event handlers.- Event Handler: A method in a subscriber class that matches the event delegate's signature.
Declaring and Raising Events
The publisher class declares the delegate and the event.
// Define a delegate for the event
public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e);
// Custom EventArgs class
public class ThresholdReachedEventArgs : EventArgs
{
public int Threshold { get; }
public ThresholdReachedEventArgs(int threshold)
{
Threshold = threshold;
}
}
public class Counter
{
private int count;
private readonly int threshold;
// Declare the event using the delegate
public event ThresholdReachedEventHandler ThresholdReached;
public Counter(int threshold)
{
this.threshold = threshold;
}
public void Add(int val)
{
count += val;
if (count >= threshold)
{
// Raise the event
OnThresholdReached(new ThresholdReachedEventArgs(threshold));
}
}
// Protected virtual method to raise the event
protected virtual void OnThresholdReached(ThresholdReachedEventArgs e)
{
ThresholdReachedEventHandler handler = ThresholdReached;
handler?.Invoke(this, e); // Safely invoke the delegate
}
}
Subscribing to Events
Subscriber classes register their methods as event handlers.
public class Program
{
static void Main(string[] args)
{
Counter counter = new Counter(5);
// Subscribe to the event
counter.ThresholdReached += Counter_ThresholdReached;
Console.WriteLine("Adding 2...");
counter.Add(2);
Console.WriteLine("Adding 3...");
counter.Add(3); // This will trigger the event
// Unsubscribe from the event (optional, but good practice)
counter.ThresholdReached -= Counter_ThresholdReached;
}
// Event handler method
static void Counter_ThresholdReached(object sender, ThresholdReachedEventArgs e)
{
Console.WriteLine($"The threshold of {e.Threshold} was reached!");
// You can access the sender object if needed:
// Counter c = (Counter)sender;
}
}
Note on Event Naming Convention
It's a common convention to name event handler delegates with the suffix EventHandler and the event itself with the suffix Event. The protected virtual method to raise the event is typically named OnEventName.
When to Use Delegates and Events
- Callbacks: Executing code when an asynchronous operation completes or a certain condition is met.
- Observer Pattern: Allowing objects to subscribe to changes in another object's state.
- Decoupled Communication: Enabling components to interact without direct dependencies on each other.
- User Interface Programming: Handling user interactions like button clicks, mouse movements, etc.
Tip: Using Lambda Expressions and Anonymous Methods
C# supports lambda expressions and anonymous methods, which can be used to define event handlers inline, simplifying code for short operations.
counter.ThresholdReached += (sender, e) =>
{
Console.WriteLine($"Lambda handler: Threshold {e.Threshold} hit!");
};
Warning: Potential for Memory Leaks
If subscribers do not unsubscribe from events when they are no longer needed (especially in long-lived objects like singletons or static event sources), it can lead to memory leaks because the publisher holds a reference to the subscriber.