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

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

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.