Delegates in C#: From Basics to Advanced Event Handling

by Gautham Pai

1. Understanding Delegates in C#

Delegates in C# are a powerful feature that allows you to pass methods as arguments to other methods, assign methods to variables, and return methods from other methods. This article will walk you through different ways to use delegates, starting from basic concepts to more advanced uses.

Defining a Delegate

In C#, a delegate is a type that represents references to methods with a particular parameter list and return type. Let's look at a simple example:

// Define a delegate type
public delegate int Add(int x, int y);

public class Program
{
  private static int Sum(int x, int y)
  {
    return x + y;
  }
  
  public static void Main()
  {
    // Create a delegate instance and assign the Sum method to it
    Add add = Sum;
    Console.WriteLine(add(2, 3)); // Output: 5
  }
}

In this example:

  • Add is a delegate type that can reference methods taking two int parameters and returning an int.
  • We assign the Sum method to the add delegate and call it, which outputs the result of adding 2 and 3.

Functions Passed to Functions

Delegates can be passed as parameters to other methods. This is useful when you need to execute different methods based on some condition.

public class Calculator
{
  public int Add(int x, int y) => x + y;
  public int Subtract(int x, int y) => x - y;
}

public delegate int Operation(int x, int y);

public class Program
{
  public static void ExecuteOperation(Operation op, int x, int y)
  {
    Console.WriteLine(op(x, y));
  }

  public static void Main()
  {
    var calc = new Calculator();
    ExecuteOperation(calc.Add, 2, 3);      // Output: 5
    ExecuteOperation(calc.Subtract, 2, 3); // Output: -1
  }
}

Here, ExecuteOperation takes an Operation delegate and executes it. You can pass different methods to ExecuteOperation to perform different calculations.

Returning Functions from Functions

You can also return a delegate from a method. This allows for greater flexibility in how methods are called.

public delegate void BarDelegate();

public class Program
{
  private static BarDelegate Foo()
  {
    void Bar()
    {
      Console.WriteLine("Bar");
    }
    return Bar;
  }

  public static void Main()
  {
    BarDelegate p = Foo();
    p(); // Output: Bar
  }
}

In this example, Foo returns a BarDelegate, which is a delegate pointing to the Bar method. The returned delegate is then invoked in the Main method.

Lambda Expressions with Delegates

Lambda expressions provide a concise way to create delegates. They can be used for single-line functions or more complex expressions.

public delegate int Add(int x, int y);

public class Program
{
  public static void Main()
  {
    // Lambda expression with multiple lines
    Add add = (int x, int y) =>
    {
      return x + y;
    };
    Console.WriteLine(add(2, 3)); // Output: 5

    // Lambda expression with a single line
    Add addShort = (int x, int y) => x + y;
    Console.WriteLine(addShort(2, 3)); // Output: 5
  }
}

You can use lambda expressions to define the behavior of delegates in a more compact form.

Using Delegates with Dynamic Types

In C#, you can use the dynamic type to create more flexible code. This section explores how to use dynamic in place of explicitly defining delegate types.

Dynamic Delegates

public class Program
{
  public static void ExecuteOperation(dynamic op, int x, int y)
  {
    Console.WriteLine(op(x, y));
  }

  public static void Main()
  {
    dynamic add = new Func<int, int, int>((x, y) => x + y);
    dynamic subtract = new Func<int, int, int>((x, y) => x - y);

    ExecuteOperation(add, 2, 3);       // Output: 5
    ExecuteOperation(subtract, 2, 3);  // Output: -1
    ExecuteOperation(8, 2, 3);         // Runtime error
  }
}

In this example:

  • We use dynamic to declare add and subtract as Func types.
  • ExecuteOperation can now take dynamic parameters, making the method more flexible.

Note: Using dynamic can lead to runtime errors if the method signatures do not match.

2. Event Handling with Delegates in C#

Delegates are essential for handling events in C#. Here’s a basic example of how to use delegates to create an event handler.

Implementing an Event Handler

public class Button
{
  public event EventHandler OnClick;

  public void Click()
  {
    OnClick?.Invoke(this, EventArgs.Empty);
  }
}

public class Program
{
  private static void EventHandler1(object sender, EventArgs e)
  {
    Console.WriteLine("Called EventHandler1");
  }

  private static void EventHandler2(object sender, EventArgs e)
  {
    Console.WriteLine("Called EventHandler2");
  }

  public static void Main()
  {
    var b = new Button();
    b.OnClick += EventHandler1;
    b.OnClick += EventHandler2;
    b.Click();
  }
}

In this example:

  • Button class has an OnClick event, which is triggered when Click method is called.
  • We subscribe two event handlers, EventHandler1 and EventHandler2, to the OnClick event.

3. Using Delegates for Data Transformations

Delegates can be used to perform data transformations. In this section, we will implement a map function and use it for various transformations.

Implementing a Map Function

public class Program
{
  delegate int Operation(int x);

  private static IList<int> MyMap(IList<int> inputList, Operation op)
  {
    var outputList = new List<int>();
    foreach (var num in inputList)
    {
      outputList.Add(op(num));
    }
    return outputList;
  }

  public static void Main()
  {
    var nums = new List<int>() { 1, 2, 3 };
    var squaresList = MyMap(nums, x => x * x);
    foreach (var num in squaresList)
    {
      Console.WriteLine(num);
    }
    
    var cubesList = MyMap(nums, x => x * x * x);
    foreach (var num in cubesList)
    {
      Console.WriteLine(num);
    }
  }
}

Here:

  • MyMap uses a delegate Operation to apply a transformation function to each element in a list.
  • We use lambda expressions to define the transformations for squares and cubes.

Using Built-in Select Method

using System.Linq;

public class Program
{
  public static void Main()
  {
    var nums = new List<int>() { 1, 2, 3 };
    
    foreach (var num in nums.Select(x => x * x))
    {
      Console.WriteLine(num);
    }
    
    foreach (var num in nums.Select(x => x * x * x))
    {
      Console.WriteLine(num);
    }
  }
}

The Select method from LINQ provides a built-in way to perform similar transformations without manually implementing a map function.

Filtering Data Using Delegates

Delegates can also be used to filter data based on specific criteria.

public class Program
{
  private static bool isOdd(int num) => num % 2 == 1;

  public static void Main()
  {
    var nums = new List<int>() { 1, 2, 3, 4, 5 };
    foreach (var num in nums.Where(isOdd))
    {
      Console.WriteLine(num);
    }
  }
}

In this example:

  • The isOdd function is used to filter odd numbers from a list using Where method.

Using Lambda Expressions for Filtering

public class Program
{
  public static void Main()
  {
    var nums = new List<int>() { 1, 2, 3, 4, 5 };
    foreach (var num in nums.Where(num => num % 2 == 1))
    {
      Console.WriteLine(num);
    }
  }
}

Lambda expressions simplify the filtering process by defining the criteria inline.

Joining Data with Delegates

Delegates can also be used to join different data sources. Here’s how to use them in practice.

public class Contact
{
  public string? Name { get; set; }
  public string? City { get; set; }
}

public class Country
{
  public string? City { get; set; }
  public string? CountryName { get; set; }
}

public class ContactWithCountry
{
  public string? ContactName { get; set; }
  public string? Country { get; set; }

  public override string ToString() => $"Contact: {ContactName}, Country:

 {Country}";
}

public class Program
{
  public static void Main()
  {
    var contacts = new List<Contact>
    {
      new Contact { Name = "Alice", City = "New York" },
      new Contact { Name = "Bob", City = "Paris" },
      new Contact { Name = "Charlie", City = "London" }
    };

    var countries = new List<Country>
    {
      new Country { City = "New York", CountryName = "USA" },
      new Country { City = "Paris", CountryName = "France" },
      new Country { City = "London", CountryName = "UK" }
    };

    var contactsWithCountries = from contact in contacts
                                join country in countries on contact.City equals country.City
                                select new ContactWithCountry
                                {
                                  ContactName = contact.Name,
                                  Country = country.CountryName
                                };

    foreach (var c in contactsWithCountries)
    {
      Console.WriteLine(c);
    }
  }
}

Anonymous Types and their use in Transforms

In C#, anonymous types provide a convenient way to encapsulate a set of read-only properties into a single object without having to explicitly define a class. They are particularly useful for quickly grouping data in LINQ queries. Here’s an example that demonstrates how to use anonymous types in conjunction with LINQ to join data from different collections.

Here is the above code making use of anonymous types:

public class Contact
{
  public string? Name { get; set; }
  public string? City { get; set; }
}

public class Country
{
  public string? City { get; set; }
  public string? CountryName { get; set; }
}

public class Program
{
  public static void Main()
  {
    List<Contact> contacts = new List<Contact>
    {
      new Contact { Name = "Alice", City = "New York" },
      new Contact { Name = "Bob", City = "Paris" },
      new Contact { Name = "Charlie", City = "London" }
    };

    List<Country> countries = new List<Country>
    {
      new Country { City = "New York", CountryName = "USA" },
      new Country { City = "Paris", CountryName = "France" },
      new Country { City = "London", CountryName = "UK" }
    };

    var contactsWithCountries = from contact in contacts
                                join country in countries on contact.City equals country.City
                                select new
                                {
                                  ContactName = contact.Name,
                                  Country = country.CountryName
                                };
    foreach (var c in contactsWithCountries)
    {
      Console.WriteLine(c);
    }
  }
}

4. Func Delegates

C# provides predefined delegates, such as Func, which can be used to replace custom delegates for common patterns.

public delegate TResult Func<in T, out TResult>(T arg);

Using Func, you can simplify the previous map function:

private static IList<T2> MyMap<T1, T2>(IList<T1> inputList, Func<T1, T2> op)
{
    // Implementation remains the same
}

5. Multicast Delegates

In C#, delegates can also be multicast, meaning that a single delegate instance can point to multiple methods. When such a delegate is invoked, all the methods it references are executed in sequence, in the order they were added. This is particularly useful when you want to perform multiple operations in response to a single event or action.

Consider the following example:

using System;

delegate void MyDelegate(int num);

class Program
{
    static void PrintNumber(int num) // Declare a method called PrintNumber
    {
        Console.WriteLine("Number: " + num);
    }

    static void MultiplyByTwo(int num) // Declare a method called MultiplyByTwo
    {
        Console.WriteLine("Result: " + num * 2);
    }

    static void Main(string[] args)
    {
        MyDelegate d = new MyDelegate(PrintNumber); // Create a new instance of MyDelegate and assign it to PrintNumber
        MyDelegate d2 = new MyDelegate(MultiplyByTwo); // Create a new instance of MyDelegate and assign it to MultiplyByTwo
        MyDelegate d3 = d + d2; // Add d2 to d, creating a multicast delegate

        d3(5); // Invoke the multicast delegate, both PrintNumber and MultiplyByTwo will be executed
    }
}

In this code:

  1. Defining the delegate: MyDelegate is defined as a delegate that takes a single int parameter and returns nothing (void).

  2. PrintNumber method: The PrintNumber method takes an integer and prints it to the console.

  3. MultiplyByTwo method: The MultiplyByTwo method takes the same integer but prints the result of multiplying it by 2.

  4. Multicast delegate setup:

    • d is a delegate instance pointing to PrintNumber.
    • d2 is a delegate instance pointing to MultiplyByTwo.
    • d3 = d + d2 creates a multicast delegate by combining d and d2. Now d3 references both methods.
  5. Invoking the multicast delegate:

    • When d3(5) is called, both PrintNumber and MultiplyByTwo are invoked, one after the other. The output is:
      Number: 5
      Result: 10
      

    The first method prints Number: 5, and the second method prints Result: 10 (5 multiplied by 2).

Thus, multicast delegates allow you to invoke multiple methods through a single delegate, providing a way to chain operations together in a clean and organized manner.

Test Your Knowledge

No quiz available

Tags