Delegates, Events and Lambda Expressions in C# — Part 1

Omar Barguti
8 min readApr 15, 2021

--

What are Delegates
Delegates are one of the most powerful features of the C# language. When implemented correctly, they allow developers to implement applications that are loosely coupled and easily scalable. However, they can be difficult to understand, especially when encountered for the first time. But with time and some practice, the concept will become clear and can be easily integrated in any design when needed.

A delegate can be thought of as an object that references a method. Just like any object in C#, delegates can be passed-in as parameters into other methods and constructors where they can get executed. The method associated with a given delegate is assigned dynamically at run-time, which allows the client to customize and tailor the behavior of a given object. Most of the time, other approaches can be taken to accomplish the same goals as delegates. However, utilizing delegates can be the best approach in certain situations, which is why they should always be considered as a viable option. Many developers, especially those with a strong Javascript background perceive delegates are callback functions provided to methods in the parameters to be executed when necessary.

Uses of Delegates
Delegates are used heavily throughout the native .NET Framework Library. They are the backbone of all WPF forms as all UI events are tied to delegates internally. Delegates serve as a blue-print for event handlers and define the structure of the event args to be passed-in to the event handlers. Also, Delegates work nicely with Lambda Expressions which results in a terse optimized code. .NET also includes pre-defined generic delegates to be used such as Action and Func<t,tresult>.</t,tresult>

Declaring a Delegate
Delegates are declared using the ‘delegate’ keyword. The declaration of a delegate is identical to the declaration of a method signature in addition to adding the ‘delegate’ keyword. However, delegates are declared as types that need to be instantiated. Moreover, the declaration of a delegate is typically done directly within the namespace outside of any classes, even tough it is possible to declare a delegate internally within a class. There is no particular convention to where delegates are declared within the code as some declare them in a separate files while others declare them in the same file they are being used. An example of a delegate declaration that is void and takes in two integers looks like the following:

public delegate void SampleDelegate(int a, int b);

Instantiating a Delegate
Once a delegate is declared, it can be instantiated like any other object in C#. However, the constructor of a delegate accepts either a Lambda expression, a reference to another delegate instance, and anonymous function or a defined method. However, the parameter signature of any of the delegates or functions should generally match. Otherwise a compiler error could be thrown. There are rules for covariance and contra-variance that one should abide by if any of the parameters are not exactly the same.

// Delegate Declaration
public delegate void SampleDelegate(int a, int b);
// A Method with signature that matches the delegate
public void Add(int a, int b)
{
Console.Writeline(a + b);
}
public static void Main(string[] args)
{
// An instance of a delegate initialized with a Lambda Expression
var del1 = new SampleDelegate((a, b) => Console.WriteLine(a + b));
// An instance of a delegate with a reference to the Add method declared above
var del2 = new SampleDelegate(Add);
// An instance of a delegate initialized with another delegate
var del3 = new SampleDelegate(del2);
// An instance of a delegate initialized with an anonymous function
var del4 = new SampleDelegate(delegate(int a, int b) { Console.WriteLine("In an Anonymous Function"); });
}

Behind the Scenes of Delegates
Internal to the C# language, there is a system base class called ‘Delegate’. This base class contains several important properties for the delegate such as the ‘Method’ property, which is a reference to the method bound to the delegate. It also contains the property ‘Target’ which is a reference to the object in which the bound method resides in. But perhaps the most important property in the ‘Delegate’ base class is the GetInvocationList() method. This is a list of all delegates that are chained with the current delegate in order to be invoked in the order in which they are ordered in the list. The ‘Delegate’ system base class is inherited by the ‘MulticaseDelegate’ class which provides the base class by it’s invocation list property. All custom delegates in C# extend the ‘MulticaseDelegate’ base class. However, one cannot extend the ‘Delegate’ or the ‘MulticaseBaseclasses’ directly. This will cause a compiler error. One must always use the ‘delegate’ keyword to notify the compiler to perform it’s ‘magic’ under the covers.

Chaining Delegates
Delegates can be chained or combined with one another. When two delegates are combined, invoking one of them will invoke both delegates at the same time. By combining delegate A with Delegate B, Delegate A is added to the invocation list of delegate B. By invoking Delegate B, all delegates on it’s invocation list are invoked in the order they are listed. This feature comes in handy when using events to trigger multiple method handlers when an event is raised.

The invocation lists of delegates can be manipulated using the build-in C# ‘+=’ and ‘-=’ assignment operators. The ‘=’ operator can also be used, but it will override any preexisting entries in the invocation list.

public static void Main(string[] args) 
{
// Instances of a delegates are initialized
var delA = new SampleDelegate((a, b) => Console.WriteLine(a + b));
var delB = new SampleDelegate(delA);
var delC = new SampleDelegate(delB);
var delD = new SampleDelegate(delegate(int a, int b) { Console.WriteLine("In an Anonymous Function"); });
// Delegates A, B, and C are added to delegate D's invocation list
delD += delA;
delD += delB;
// Delegate A is removed from delegate D's invocation list
delD -= delA;
// Delegate D is re-initialized by delegate C overriding any preexisting entries in delegate D's invocation list
delD = delC;
}

Chained Delegate Functions with a Return Value
When multiple delegates are chained and each return a value, the value returned by the last delegates on the invocation list is returned. This is how the .NET platform resolves the conflict when multiple values are returned. It is also known as ‘last wins’ policy.

// Delegate that returns an integer is declared
public delegate int SampleDelegate(int a, int b);
public class Sample
{
public static void Main(string[] args)
{
// Delegate A returns the sum of two integers
var delA = new SampleDelegate((a, b) => a + b);

// Delegate B returns the product of two integers
var delB = new SampleDelegate((a, b) => a * b);

// Delegate B is chained by Delegate A and is added to the invocation list
delA += delB;
// Invoking delegate A returns the value of the last delegate on the invocation list, which is the value from delegate B
Console.WriteLine(delA(2, 4)); // Prints 8
}
}

Invoking a Delegate
Once a delegate instance is created, it can be easily invoked just like any method in the system. If the delegate requires any parameters, the parameters must be supplied at compile time like with any methods.

public static void Main(string[] args) 
{
// Instances of a delegates are initialized
var delA = new SampleDelegate((a, b) => a + b);
// Delegate A is invoked just like any function. The return value is captured in a variable like any function in C#
var result = delA(2, 3);
}

Example Using Delegates
The example below creates an Employee Service that operates over a list of Employees. The service maintains a list of employees and exposes a public constructor that takes in a list of employees to initialize the service and a PrintEmployees method that prints the names of the employees in the initialized list:

EmployeeService.cs

public class EmployeeService
{
public IEnumerable Employees { get; set; }
public EmployeeService(IEnumerable employees)
{
Employees = employees;
}
public static void PrintEmployees(IEnumerable employees)
{
foreach (var employee in employees)
{
Console.WriteLine(employee.Name);
}
}
}

The Employee class defines a number of attributes associated with a given employee such as the Name, Age, whether a given employee is a manager and whether they are a minor.

Employee.cs

public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public bool IsManager { get; set; }
public bool IsFullTime { get; set; }
}

Adding Filtering Capability to EmployeeService
Assume that one desires to implement a filtering capability that enables the EmployeeService to filter the list of Employees by the Age, IsManager and IsFullTime attributes. There are many possible approaches to implement this feature and each has it’s own advantages and disadvantages. However, one approach would be to define a FilterEmployees() method that invokes a delegate of type Filter to filter through the list of Employees. The Filter delegate can be assigned a function dynamically to filter through the list of employees and return the required filtered list of Employees. At the time the FilterEmployees() method is invoked, it would not be known which handler function is bound to the delegate. Below is the declaration of the Filter delegate:

public delegate bool Filter(Employee employee);

Once the Filtering is declared, the FilterEmployees() method is defined to take in an instance of the delegate and use it to filter through the list of employees:

public IEnumerable FilterEmployees(Filter employeesFilter)
{
return Employees.Where(e => employeesFilter(e));
}

Next, several filtering functions are defined in the EmployeeService that can be assigned to the Filter delegate. These functions take an Employee object and return a boolean indicating whether the employee matches the filtering criteria or not.

public static bool IsManager(Employee employee)
{
return employee.IsManager;
}
public static bool IsFullTime(Employee employee)
{
return employee.IsFullTime;
}
public static bool IsMinor(Employee employee)
{
return employee.Age < 18;
}

Finally, in the Main() method, the list of Employees is initialized with sample data of various employees. The Filter delegate is assigned one of the filtering functions dynamically each time before calling the FilterEmployees() method. Each time the method is called, the list of employees is filtered by the function bound to the delegate. Complete code below:

EmployeeService.cs

public delegate bool Filter(Employee employee);public class EmployeeService
{
public IEnumerable Employees { get; set; }
public EmployeeService(IEnumerable employees)
{
Employees = employees;
}
public static void PrintEmployees(IEnumerable employees)
{
foreach (var employee in employees)
{
Console.WriteLine(employee.Name);
}
}
public static void Main(string[] args)
{
var employees = new List
{
new Employee() { Name = "Arch Stanton", Age = 44, IsFullTime = true, IsManager = false },
new Employee() { Name = "Bill Carson", Age = 42, IsFullTime = true, IsManager = true },
new Employee() { Name = "D. Harry", Age = 16, IsFullTime = false, IsManager = false }
};
var employeeService = new EmployeeService(employees);
PrintEmployees(employeeService.Employees);
var filterDelegate = new Filter(IsManager); var filteredEmployees = employeeService.FilterEmployees(filterDelegate);
PrintEmployees(employeeService.FilterEmployees(filterDelegate));
filterDelegate = IsFullTime;
PrintEmployees(employeeService.FilterEmployees(filterDelegate));
filterDelegate = IsMinor;
PrintEmployees(employeeService.FilterEmployees(filterDelegate));
}
public IEnumerable FilterEmployees(Filter employeesFilter)
{
return Employees.Where(e => employeesFilter(e));
}
public static bool IsManager(Employee employee)
{
return employee.IsManager;
}
public static bool IsFullTime(Employee employee)
{
return employee.IsFullTime;
}
public static bool IsMinor(Employee employee)
{
return employee.Age < 18;
}
}

--

--

Omar Barguti
Omar Barguti

Written by Omar Barguti

An enthusiastic architect and full-stack developer with many years working with enterprise software and cloud services in multiple domains.

No responses yet