Delegates, Events, Asynchronous Programming in CSharp
In C#, understanding core concepts like delegates, events, and asynchronous programming is crucial. This article explores these features and provides real-world examples of their usage.

Delegates
Delegates in C# are like type-safe function pointers. A delegate defines a method signature, and you can assign any method with a compatible signature to the delegate.
For instance, a delegate that represents a function that takes in an int and returns an int could be:
public delegate int Transform(int value);
We could use this delegate to reference a method that doubles an integer:
public int Double(int x)
{
return x * 2;
}
Transform transform = Double;
int result = transform(4); // result is 8
Delegates: The Remote Controls
Let’s think about a universal remote control. This remote can be programmed to control various devices, such as your TV, DVD player, or stereo system. You press a button, and the remote executes the appropriate action on the device it’s currently pointed at. This is akin to a delegate in C#. A delegate is like a remote control button, which knows what method it should call but doesn’t know what object it will be executed on until runtime.
For example, we could define a delegate that represents a method that takes in an integer and returns void.
// Define a delegate
public delegate void MyDelegate(int number);
// Define methods that match the delegate signature
public void AddFive(int number)
{
Console.WriteLine(number + 5);
}
public void SubtractTwo(int number)
{
Console.WriteLine(number - 2);
}
// Now we can use our delegate
public void UseDelegate()
{
// Create an instance of the delegate
MyDelegate del = AddFive;
del(10); // Outputs 15
// Change what method the delegate is pointing to
del = SubtractTwo;
del(10); // Outputs 8
}
In the above code, MyDelegate is a delegate that we’ve defined which can represent any method that takes in an integer and returns void. We can then set del to point to different methods at runtime, like changing what device a universal remote is controlling.
The function that the button on the remote control triggers is indeed “bound” or “mapped” to that button, similar to how a method is associated with a delegate in C#. However, in the context of the analogy, the main point is to illustrate how delegates can be pointed towards different methods at runtime, like how a universal remote can control different devices.
So, in the analogy, the “binding” of a button to a function is done dynamically, depending on the device the universal remote is currently set to control (TV, DVD player, etc.). This mirrors the way that a delegate can be associated with different methods at runtime in C#.
To make the analogy clearer:
- Devices (TV, DVD player, etc.) = Different methods in your program
- Universal Remote = Delegate
- Button press = Invocation of the delegate
- Action performed by the device (change channel, play/pause, etc.) = The method that gets executed when you invoke the delegate
So, when you press a button on the universal remote (invoke the delegate), it sends a signal to the device it’s currently set to control (the method the delegate is currently holding a reference to), which then performs the appropriate action (the method executes).
Usage of Delegates
Delegates allow you to encapsulate a method in an object, making it easy to pass methods as parameters, store them in data structures, or even delay their execution. Delegates truly shine when you need more flexibility in your code, such as:
-
Event handling: This is probably the most common usage of delegates. They provide a way of notifying other parts of your program when certain actions have taken place (user clicking a button, a file finishing downloading, etc.). They allow you to “plug in” code that will be executed when the event happens.
-
Asynchronous programming: Delegates are used heavily in asynchronous programming (running code on separate threads). They allow you to specify a method that should be run on a separate thread, and can even provide a callback method that should be run when the asynchronous operation is completed.
-
Callback methods: If you have a method that’s time-consuming (such as data processing or complex calculations), you can pass a delegate to be called when the operation is done. This way, the method can alert your program that it’s finished and maybe pass back some result.
-
Defining method behavior at runtime: There might be cases where you want to define the behavior of some part of your code at runtime. Delegates can do this by “standing in” for a method that is assigned at runtime.
-
LINQ and Lambda Expressions: Language Integrated Query (LINQ) and lambda expressions are built on the delegate model.
Event Handling
One of the most common uses of delegates in C# is for event handling. An event in C# is a way for a class to provide notifications to clients of that class when some interesting event happens. An event is a special kind of multicast delegate that can only be invoked from within the class or struct where it is declared.
Consider a simple Button class:
public class Button
{
// Declare the delegate (if not already declared)
public delegate void ClickHandler(object sender, EventArgs e);
// Declare the event using the delegate
public event ClickHandler Click;
public void SimulateClick()
{
// Raise the event
Click?.Invoke(this, EventArgs.Empty);
}
}
And a User class that responds to the click:
public class User
{
public void RespondToClick(object sender, EventArgs e)
{
Console.WriteLine("Button clicked!");
}
}
You can wire up the Click event to the RespondToClick method like this:
Button button = new Button();
User user = new User();
// Subscribe to the Click event
button.Click += user.RespondToClick;
// Simulate a button click
button.SimulateClick(); // Outputs: "Button clicked!"
In this example, when the Button is clicked (via SimulateClick), it raises the Click event, and the User’s RespondToClick method gets called.
Delegates in Asynchronous Programming
Delegates also play an essential role in asynchronous programming. The idea behind asynchronous programming is to allow a unit of work to run separately from the main application thread and notify the calling thread about its completion, failure, or progress. This pattern is especially useful for I/O-bound tasks to prevent your application from blocking (waiting) while the I/O operation completes.
Before C# 5.0, we used the BeginInvoke and EndInvoke methods on the delegate for asynchronous operations. For example:
public delegate int BinaryOperator(int a, int b);
public int Add(int a, int b)
{
Thread.Sleep(5000); // Simulate a long-running operation
return a + b;
}
BinaryOperator add = Add;
// Call the Add method asynchronously
IAsyncResult asyncResult = add.BeginInvoke(5, 6, null, null);
// Do other things while Add is running...
// Now we need the result
int result = add.EndInvoke(asyncResult); // Blocks if Add is not finished
Console.WriteLine(result); // Outputs: 11
In this example, we simulate a long-running operation with Thread.Sleep. We call the Add method asynchronously with BeginInvoke and continue doing other things without waiting for the Add method to complete. When we need the result, we call EndInvoke, which blocks if the Add method is still running.
However, this older asynchronous model has been largely replaced with the Task-based model in recent versions of C#.
Asynchronous Programming with the Task-based Asynchronous Pattern (TAP)
Starting with C# 5.0, the Task-based Asynchronous Pattern (TAP) is the recommended way to represent asynchronous operations. The core of TAP is the Task and Task
Here is the equivalent Add operation from before, but now using Task:
public async Task<int> AddAsync(int a, int b)
{
await Task.Delay(5000); // Simulate a long-running operation
return a + b;
}
// Call the Add method asynchronously
Task<int> addTask = AddAsync(5, 6);
// Do other things while AddAsync is running...
// Now we need the result
int result = await addTask;
Console.WriteLine(result); // Outputs: 11
In this example, the AddAsync method is declared with the async keyword, which allows us to use the await keyword inside the method. The await keyword causes the method to asynchronously wait for the Task to complete.
When we call AddAsync, it returns a Task
One crucial thing to understand here is that using await does not block the current thread. Instead, it signs up the rest of the method as a continuation and returns control to the caller of the async method.
The Context in Asynchronous Programming
When you await a task, the context (like a UI thread) is captured, and the method resumes in this context. In UI-based applications, this context is the UI thread. Capturing the context can be controlled by the ConfigureAwait method. If you do not want to capture the context, you can use ConfigureAwait(false), which can increase performance.
For instance, consider the following code in a WPF or Windows Forms application:
private async void Button_Click(object sender, RoutedEventArgs e)
{
await LongRunningOperationAsync();
// This code runs on the original context (UI thread)
this.Button.IsEnabled = false;
}
private async Task LongRunningOperationAsync()
{
// This code runs on the original context (UI thread)
await Task.Delay(5000).ConfigureAwait(false);
// This code runs on a ThreadPool thread
}
In this example, we simulate a long-running operation with Task.Delay. We call ConfigureAwait(false) to avoid capturing the UI context. After the delay, the rest of the method runs on a ThreadPool thread. However, the Button_Click method, which awaits LongRunningOperationAsync, continues on the UI thread after the await, allowing it to safely access UI elements.
Delegates for Callback Methods
In C#, a callback method is a function that you pass as a parameter to another function. This passed function is then invoked at some point in the future. This allows you to inject behavior into methods that need to perform a specific action during their execution.
Consider the following delegate and method:
public delegate void Del(string message);
public void Process(Del callback)
{
callback("Hello, World!");
}
In this code, the Process method accepts a delegate as a parameter. It can invoke this delegate (the callback method) whenever it needs to:
public void PrintMessage(string message)
{
Console.WriteLine(message);
}
Del del = PrintMessage;
Process(del); // Outputs: "Hello, World!"
In this example, PrintMessage is the callback method. We wrap it in a delegate and pass it to Process. The Process method then invokes the callback method when it’s ready.
Defining Method Behavior at Runtime using Delegates
Delegates allow developers to change method behavior at runtime. This is done by assigning a method to a delegate, which can then be invoked at some point in the future. This allows you to decide which method to call at runtime, based on conditions in your code.
Consider the following example:
public delegate void GreetingDelegate(string name);
public void SayHello(string name)
{
Console.WriteLine($"Hello, {name}!");
}
public void SayGoodbye(string name)
{
Console.WriteLine($"Goodbye, {name}!");
}
GreetingDelegate greeting;
if (DateTime.Now.Hour < 12)
{
greeting = SayHello;
}
else
{
greeting = SayGoodbye;
}
greeting("Alice");
In this example, we define a delegate GreetingDelegate and two methods SayHello and SayGoodbye that match the delegate’s signature. Depending on the time of day, we assign either SayHello or SayGoodbye to the greeting delegate, effectively deciding at runtime which method to call.
Delegates in LINQ and Lambda Expressions
Delegates are the foundation of LINQ (Language Integrated Query) and lambda expressions in C#. LINQ allows you to query and manipulate data from a variety of data sources like arrays, collections, XML, databases, etc., in a type-safe, declarative way.
Here’s an example where we use LINQ and a lambda expression to filter a list of numbers:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Use a lambda expression to define a delegate that filters out odd numbers
IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var number in evenNumbers)
{
Console.WriteLine(number); // Outputs: 2, 4, 6, 8, 10
}
In this example, we use the Where method, which is a LINQ extension method for IEnumerable
In conclusion, delegates are a crucial part of C#, enabling developers to write flexible, dynamic, and efficient code. By understanding how to use delegates, developers can take advantage of some of the most powerful features that C# has to offer.