Delegates in C#

Sushant PantSushant Pant
12 min read

Introduction

Delegates in C# are like the instructions you give to different pieces of code, telling them what to do and how to work together. Delegates were introduced in C# to provide a flexible and powerful mechanism for implementing callback functions, event handling, functional programming and for asynchronous programming.

Use Cases

  • Callback function: Invoke one piece of code after an event or after execution of some code.

    Eg: Imagine a weather app where you need to fetch real time weather data from 3rd party sources. Instead of freezing the entire application while waiting for data, initiating an asynchronous data retrieval process can be optimal. You provide a callback function (delegate) that processes the weather once it is fetched. This ensures that your application remains responsive, and the weather information is seamlessly integrated when available.

  • Event handling: Communicate and respond to certain occurrences, specially in event driven architecture.

    Eg: You have two buttons in your weather app interface: one for Celsius and another for Fahrenheit. Each button is associated with a click event. When a user clicks the Celsius button, the Celsius button click event is triggered which shows the data in °C.

  • Functional programming: Delegates allow passing of function or methods as parameter which is the main idea behind functional programming. Think of it like legos. Just like Lego pieces that can be interchanged and combined to create various structures, delegates enable the passing of functions or methods as parameters to other functions. This paradigm is the core principle of functional programming.

  • Asynchronous programming: Delegates allow you to define callback methods that get invoked when an asynchronous task completes.

    Eg: Imagine you're uploading photos to the cloud. Without asynchronous help, you'd have to wait for each photo to be uploaded before doing anything else. Asynchronous operation helps your app to start sending photos and not wait around. So, you're free to use the app while all your pictures get uploaded to the cloud in the background.

Example

By using delegate keyword with return type, delegate name, and parameters, you declare a delegate. A simple example portraying use of delegate:

delegate int Addition(int a, int b);
// method that takes two numbers (a and b) and returns their sum.
private int AddNumbers(int a, int b)
{
    return a + b;
}
public void OperateAddNumbers()
{
    // create an instance of the Addition delegate and point it to the private AddNumbers method.
    Addition addition = new Addition(AddNumbers);
    // invokes the delegate, calling the AddNumbers method with following arguments.
    int ret = addition(1,2);
    Console.WriteLine($"Result of addition: {ret}");
}

Delegates are like messengers that deliver your instructions to someone who knows how to perform a specific task. In this example, we declare a delegate named Addition that represents the idea of adding two numbers. The private method AddNumbers is the actual source which computes in this task. Now, when the public method OperateAddNumbers wants to add numbers, it creates a helpful messenger (delegate) named addition and tells it, 'Go to AddNumbers, and ask it to add 1 and 2.' The messenger delivers the request, and we get the result.

So, delegates simplify our code by acting as messengers between different parts of our program, making sure the right source handle specific tasks.

Another scenario, imagine you have a notification service that can send messages, and you have users who want to receive those messages. A messenger is the one who delivers news to everyone.

We will build a NotificationService class that consists of the delegate NotificationHandler, messenger to notify of type NotificationHandler and the message sender SendNotification.

public class NotificationService
{
    // Delegate handling notification
    public delegate void NotificationHandler(string message);

    // "messenger" to notify
    public NotificationHandler Notify;

    // send notification 
    public void SendNotification(string message)
    {
        // ensure that there are subscribers to send notification
        if(Notify != null)
        {
            Notify(message);
        }
    }
}

Above, we declare a special kind of helper named NotificationHandler (a delegate). It's like saying, "Hey, I'm creating a messenger that can carry messages." Notify is our messenger. It's the one who will deliver the messages. When the SendNotification method is called, it checks if there is a messenger (Notify). If yes, it tells the messenger to deliver the message. User receives the notification through the messenger who ensures that the message is sent to every subscribed user.

// Class portraying subscriber.
public class Subscriber
{
    public string Username { get; set; }

    // Imagine this as a smartphone, showing user notification.
    public void ReceiveNotification(string message)
    {
        Console.WriteLine($"New notification for you {Username}: {message}");
    }
}

Each Subscriber has a method ReceiveNotification, which is what happens when the messenger delivers a message. In this case, it shows the message on the console.

// create notification service
NotificationService notificationService = new NotificationService();
// create subscribers using collection initializer
List<Subscriber> listOfSubscriber = new List<Subscriber>()
{
    new Subscriber(){ Username = "Priti"},
    new Subscriber(){ Username = "Jon"},
    new Subscriber(){ Username = "Ram"}
};

// Subscribe Users to the Notification Service 
foreach (Subscriber subscriber in listOfSubscriber)
{
    notificationService.Notify += subscriber.ReceiveNotification;
}

// random amount of voucher 
Random randAMount = new Random();
int discountAmount = randAMount.Next(10, 251);

// send notification to the subscribers who are subscribed. 
notificationService.SendNotification($"Here's a voucher of Rs. {discountAmount}! Enjoyyyyyy.");

Now, each time the SendNotification method is called, it generates a random discount amount and includes it in the notification message. Subscribers will receive notifications with different voucher amounts every single time. Each user in the listOfSubscribers is subscribed to the Notify event of the NotificationService , its like saying, "Hey, when there's a message, let me know, and here's what I'll do with it". When the SendNotification method is called, it triggers the Notify event, and each subscribed user's ReceiveNotification method is invoked, displaying the received message.

Output:

In simple terms, the delegate (NotificationHandler) acts as a messenger between the NotificationService and the subscribers (Subscriber). It allows the service to notify all subscribers when there's something exciting happening, like sending a voucher. So, the delegate facilitates communication and ensures everyone who's interested gets the news.

Delegates are extensively useful when implementing Observer Design Pattern and in Event-Driven architecture where an object (in our example, NotificationService) needs to notify other objects (such as Subscriber) about changes or events.

Built-in delegates

C# introduced built in delegates types Func<>, Predicate<>, and Action<> in version 2.0. Instead of writing delegates yourself, these built in delegate types can be more expressive way to work with delegates. Func<>, Predicate<>, and Action<> are of generic delegate type meaning they can be used with different types of parameter and return types. This makes them highly reusable in various scenarios reducing the need for custom delegate. It also seamlessly integrate with LINQ expressions, making it more expressive on collections.

I often skip writing my own custom delegates. Func<>, Predicate<>, and Action<> are my preferences due to the versatility and readability.

Func<>Predicate<>Action<>
PurposeRepresents a method with parameters and a return type.Represents a method that evaluates a condition.Represents a method with parameters and no return.
Return typeHas a return type (can be any type, including void).Always returns a boolean (true or false).Does not have a return type (void).
ParametersCan have zero or more input parameters.Usually has one input parameter.Can have zero or more input parameters.
Examplepublic Func<int, int, string> AddNumbersFunc = (a, b) => (a + b).ToString();public Predicate<int> isEven = num => num % 2 == 0;public Action<int, int> printSumOfNums = (a, b) => Console.WriteLine($"Sum : {a + b}");
Example callConsole.WriteLine(concatenateNumbers(1, 4)); // Output: 5Console.WriteLine(isEven(2)); // Output: TrueprintSum(30, 20); // Output: Sum: 50
Use caseMathematical calculations, and custom logic in LINQ queries, leveraging its versatility to define functions tailored to specific needs in a single line of code.Filtering or testing conditions.Performing an action without returning a value.

The versatile: Func<>

The versatility of Func<> allows it to produce result based on the input parameter. It's a generic delegate type that can represent any method with parameters and a return type.

Where to use: you want to get result after doing something.

Example: You want to calculate the total price of items in a shopping cart.

public Func<List<int>, int> calculateTotalPriceInTheCart = items => items.Sum();
List<int> itemInCartPrices = new List<int> { 210, 450, 1500, 2600 };
int totalPrice = eg.calculateTotalPriceInTheCart(itemInCartPrices);
Console.WriteLine(totalPrice);
// Prints: 4760

Conditional crafter: Predicate<>

Predicate<> is perfect for scenarios where you need to evaluate a condition and return a boolean. Predicate<> can be used in filtering collections, conditional validation, removing elements, custom conditions in LINQ, event handling, conditional logic in algorithms, and testing conditions in unit testing, leveraging its true or false outcomes to express a wide range of conditions in a single line of code.

Where to use: you want to check some condition.

Example: You want to check if a customer is eligible for a discount based on their purchase amount. If the total amount exceed 1000, customer receives 10% discount.

public Predicate<int> isEligibleForDiscount = purchaseAmount => purchaseAmount > 1000;
bool eligible = eg.isEligibleForDiscount(totalPrice); // Result: True
Console.WriteLine($"Is eligible for discount: {eligible}");
if (eligible)
{
    Console.WriteLine($"Total price before discount: {totalPrice}");
    totalPrice = totalPrice - (int)(0.1 * totalPrice); // explicit casting. Implicit casting is not allowed from decimal to int.
    Console.WriteLine($"Total price after 10% discount: { totalPrice}");
}
/*
Prints:
Is eligible for discount: True
Total price before discount: 4760
Total price after 10% discount: 4284
*/

The silent executor: Action<>

Action<> is a go-to delegate when you need to represent a method that takes parameters but doesn't return anything (its a void). It focus on action rather than returning the values. Action<> can be used where the focus is on execution rather than computation.

Where to use: focus is on some action rather than computation.

Example: You want to log a confirmation message when a customer makes a purchase.

public Action<string> logPurchaseConfirmationMessage = message => Console.WriteLine($"Log: {message}");
eg.logPurchaseConfirmationMessage("Purchase confirmed for username12345.");
// Action: it prints the log message. 
// Log: Purchase confirmed for username12345.

Multicast delegate

Up to this point, we understood that a delegate references to a method allowing for a level of abstraction and flexibility in function invocation.

Moving on, the concept of multicast delegate is its ability to point to and invoke multiple methods concurrently. Multicast delegate can maintain a list of methods and invoke them concurrently. Its declaration is similar to regular delegate. The difference is we keep adding methods to the delegate. Basically, multicast delegate is a collection with multiple references to methods allowing it to invoke all of them when triggered at the same time.

In our previous example, Subscriber had only one method to receive message ReceiveNotification(string message). We will add another method here ReceiveAnotherNotification(string message). Think of this like apps in your smartphone. You can have bunch of them installed. You can receive messages/notifications from all of them. In our case, we can have as much notification as our subscribers want but we will invoke both of them using multicast delegate. Here is the added method:

public void ReceiveAnotherNotification(string message)
{
    Console.WriteLine($"New notification for you from other service {Username}: {message}");
}

Subscriber class now look like this:

public class Subscriber
{
    public string Username { get; set; }

    public void ReceiveNotification(string message)
    {
        Console.WriteLine($"New notification for you {Username}: {message}");
    }

    // newly added method to demonstrate multicast delegate
    public void ReceiveAnotherNotification(string message)
    {
        Console.WriteLine($"New notification for you from other service {Username}: {message}");
    }
}

Now, we will add ReceiveAnotherNotification(string message) to be included in our notify. += is used to add event handlers (methods or delegates) to an event (notify), allowing multiple subscribers to respond to an event when it occurs.

// create notification service
NotificationService notificationService = new NotificationService();
// create subscribers
List<Subscriber> listOfSubscriber = new List<Subscriber>()
{
    new Subscriber(){ Username = "Priti"},
    new Subscriber(){ Username = "Jon"},
    new Subscriber(){ Username = "Ram"}
};

// Subscribe Users to the Notification Service 
foreach (Subscriber subscriber in listOfSubscriber)
{
    notificationService.Notify += subscriber.ReceiveNotification;
    // add reference of another method to our Notify delegate.
    notificationService.Notify += subscriber.ReceiveAnotherNotification;
    // this is multicast delegate. 
}

// random amount of voucher 
Random randAMount = new Random();
int discountAmount = randAMount.Next(10, 251);

// send notification to the subscribers who are subscribed. 
notificationService.SendNotification($"Here's a voucher of Rs. {discountAmount}! Enjoyyyyyy.");

Everything looks similar to above delegate example except in the foreach loop where we added reference of ReceiveAnotherNotification in our Notify a "messenger". Now, when the SendNotification method is called, the multicast delegate Notify invokes both the ReceiveNotification and ReceiveAnotherNotification methods for each subscriber. It's like receiving notifications from multiple services or apps on your smartphone. The power of multicast delegates shines in scenarios where you want to notify multiple methods or subscribers about an event concurrently, enhancing the flexibility and extensibility of your event-driven architecture.

Summary

  1. Delegate:

    • Delegates in C# act as messengers, providing a flexible and powerful mechanism for callback functions, event handling, functional programming, and asynchronous programming.
  2. Use Cases:

    • Callback Function: Invoke code after an event or execution.

    • Event Handling: Communicate and respond to occurrences in event-driven architecture.

    • Functional Programming: Pass functions or methods as parameters.

    • Asynchronous Programming: Define callback methods for asynchronous tasks.

  3. Example:

    • NotificationService class and Subscriber class demonstrate delegates in action, facilitating communication between a service and subscribers.
  4. Built-in Delegate Types:

    • Func<>: Represents a method with parameters and a return type.

    • Predicate<>: Represents a method that evaluates a condition (always returns boolean).

    • Action<>: Represents a method with parameters and no return (its a void).

  5. Func<> :

    • Used when you want to produce a result based on input parameters.

    • Example: Calculating the total price of items in a shopping cart.

  6. Predicate<> :

    • Perfect for checking conditions and returning a boolean result.

    • Example: Checking if a customer is eligible for a discount based on purchase amount.

  7. Action<> :

    • Used when the focus is on executing actions without returning values.

    • Example: Logging a confirmation message when a customer makes a purchase.

  8. Multicast Delegates:

    • Extension of regular delegates, pointing to and invoking multiple methods concurrently.

    • Maintain a list of methods for simultaneous invocation.

    • Example: Sending notifications to subscribers using a multicast delegate in NotificationService.

  9. More :

    • Delegates simplify code by acting as messengers between different parts of the program.

    • Func<>, Predicate<>, and Action<> are built-in delegate types for added expressiveness and versatility.

    • Multicast delegates enhance flexibility and extensibility in event-driven architectures.

    • Multicast delegate can maintain a list of methods and invoke them concurrently.

Sourcecode.

12
Subscribe to my newsletter

Read articles from Sushant Pant directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sushant Pant
Sushant Pant