Introduction to Delegates

Rasheed K Mozaffar - Jul 17 - - Dev Community

Hi there!

Delegates, Funcs, and Actions are all C# words, and are used extensively throughout the language, the frameworks you use, and in almost every project you're going to work on, you'll encounter them in one form or another, so what are they, why are they used, what's their benefit, and what are the different types of Delegates do we have in C#?

Let's Get Started!

Introduction 🎬

Delegates are an important feature of C#, and they're widely used, they're considered an advanced topic and sometimes aren't taught early on, but they're one of those concepts that's once learned, you'll find yourself using a lot. I always try to keep things as simple, and as straightforward as possible so that you don't only understand the concept discussed, but also feel comfortable using it.
So in this series about delegates, we'll be exploring the concept in depth, with practical code samples, easy to understand explanations, and dedicated posts to each delegate variation we have in C#. If that sounds interesting, keep reading!

Defining Delegates 📜

To begin our trip, we'll start by a definition from Microsoft Documentation:

A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type.

In summary, a delegate is like a type that defines how a method should look like, i.e its signature and return type, and instances of that delegate can be assigned different methods as long as they comply with the signature of the delegate.

If you don't know what a method signature means, let me explain.
Take a look at this simple Sum method:


private int Sum(int x, int y) => x + y;

Enter fullscreen mode Exit fullscreen mode

The name of the method, Sum, the parameters int, int, and their order, makes up what's known as a method signature. Two methods with the exact same signature cannot co-exist in the same class, like assuming you have another Sum method that uses a different adding algorithm, will cause a compile time exception if the 2 methods shared the same name, same parameter list, and same parameters order.

NOTE: ⚠️ The name of the parameters isn't equated to in the method signature, like defining Sum again but as Sum(int a, int b) will still not work.


public int Sum(int x, int y) => x + y;

public int Sum(int a, int b) => a + b;

Enter fullscreen mode Exit fullscreen mode

This code will throw this compile time exception:
Member with the same signature is already declared

Now with the definition out of the way, let's declare our first delegate that we'll invoke the previous sum method through.

Declaring a Delegate 🛠️

To define a delegate, we need to use the following template:
[ACCESS_MODIFIER] delegate [RETURN_TYPE] [NAME]([PARAMETERS_LIST]);

Let's break it down:

  • Access Modifier: Defines the accessibility level for the delegate type, can be public, private, internal just to mention a few.
  • delegate: This keyword is a C# reserved keyword, used to denote that the declared type is a delegate type.
  • Return Type: Defines the return type of the delegate type, can be any primitive data type like int, string... Or a user defined type like CustomerDetails, Employee... In addition to void indicating the delegate returns nothing.
  • Name: A unique identifier for the delegate type.
  • Parameters List: The list of the parameters the delegate type is expecting, remember the order matters, this can be up to 254 parameters for instance methods, and 255 for static methods including this keyword.

Let's Declare Our First Delegate 🚀

In a text editor of your choice, type out this code in a console application:


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

Enter fullscreen mode Exit fullscreen mode

Now a compatible method should look like that:


int Sum(int x, int y) => x + b;

Enter fullscreen mode Exit fullscreen mode

It's got the same return type, and parameters list. Let's see how we can put these together and invoke the Sum method using our SumDelegate.

Inside Program.cs, we'll write this code:


int x = 5;
int y = 6;

SumDelegate sumDel = new SumDelegate(Sum);
int result = sumDel(x,y);

Console.WriteLine(result); // OUTPUT: 11

static int Sum(int x, int y) => x + y;

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

Enter fullscreen mode Exit fullscreen mode

NOTE: ⚠️ Please make sure to put the delegate definition at the bottom of the file, because we're using top-level statements where namespace and type declaration should go below any code that we want to execute (Any top-level statements).

This program will output 11 to the console window, which is the correct result of our addition operation. The interesting part to notice, is how we instantiated our delegate instance.

To new up a delegate, we use the new keyword, just like with classes, structs and so on. Then in the constructor invocation, we pass the target method the delegate will point to, in our case it's the Sum method. If you try to pass a method with an incompatible signature, this code will simply not compile, take a look at the following code:


SumDelegate sumDel = new SumDelegate(Sum);

...

static double Sum(double a, double b) => a + b;
// static int Sum(int a, int b) => a + b;

public delegate int SumDelegate(int a, int b);

Enter fullscreen mode Exit fullscreen mode

I replaced the old Sum with a Sum that uses double as both return type and parameters, now my editor will show this compile-time error:
Expected a method with 'int Sum(int, int)' signature

The other thing to note, is how we invoked the delegate, we did so like how we would invoke a normal method, by typing the delegate name, followed by parenthesis, passing down the parameters we want to invoke Sum with. Also, because our delegate SumDelegate returns an int, we were able to store the result inside a variable of type int too, we then used that to log the operation result to the console.

🎉 Congratulations! You declared your first delegate and invoked a method through it!

Clearing Some Confusion 🤔

I know by now, you're having the question as to why did we do all of that, just to call a method which we could've done the normal way, and got the same result, like I'm sure you're asking yourself why don't we make the code look like this:


int x = 5;
int y = 6;

int result = Sum(x, y);

Console.WriteLine(result);

static int Sum(int a, int b) => a + b;

Enter fullscreen mode Exit fullscreen mode

Wouldn't that result in the same output but with just less code, and less complexity? The answer is, you're absolutely right.
But the thing is, delegates aren't just used to invoke methods like we just did, instead, the power of delegates shines when you get to pass methods as arguments to other methods, which in normal method invocation, is simply not possible, in addition to being able to invoke multiple methods at once, using multicast delegates, defining callbacks or respond to events, all of which we're going to discuss later in this series.

Assigning Methods to Delegates 🧪

We declared a delegate earlier, and we assigned a target method to it through the constructor. However, there are other ways of doing that in C#, plus the ability to replace target methods with different ones.

We'll code a basic message logging example, which will set us up to look at multicast delegates and see them in action.

Coding a Basic Logger 🪵

We'll start by declaring a class called ConsoleLogger, which has the following code:


public static class ConsoleLogger
{
    public static void Log(string message)
    {
        Console.WriteLine($"Log: {message} - {DateTime.Now}");
    }
}

Enter fullscreen mode Exit fullscreen mode

The logger is pretty basic, it's got a single static method called Log, accepting a string message which it writes to the console as well as the current date time.

In Program.cs, I added a new delegate declaration at the bottom which has the following definition:
public delegate void LogDelegate(string message);

As you can see, the Log method matches the signature and return type of the LogDelegate, which means it's compatible with it, so what we will do now is, instantiate an instance of LogDelegate, and assign it the Log method of ConsoleLogger like this:


LogDelegate logDel = ConsoleLogger.Log;
logDel("Hello, world!");

public delegate void LogDelegate(string message);

public class ConsoleLogger
{
    public static void Log(string message)
    {
        Console.WriteLine($"Log: {message} - {DateTime.Now}");
    }
}

Enter fullscreen mode Exit fullscreen mode

That's some syntactic sugar, we didn't have to use the new keyword and invoke the constructor explicitly, we just assigned ConsoleLogger.Log to a variable of type LogDelegate right away.

If you were to run this, you would see something like this:

Output of using ConsoleLogger.Log with the LogDelegate

Ok now let's assume we introduced a new logger, called FileLogger, which also has a Log method, but instead of writing a message to the console, it rather logs it into a text file called log.txt, like this:


public class FileLogger
{
    public static void Log(string message)
    {
        File.AppendAllText("log.txt", $"Log: {message} - {DateTime.Now}" + Environment.NewLine);
    }
}

Enter fullscreen mode Exit fullscreen mode

Same thing as before, but we're just writing to a file this time instead of the console window, we log the message, with the current date time, and append a new line at the end so our next log sits nicely on a separate line.

Now change Program.cs to this:


LogDelegate logDel = ConsoleLogger.Log;
logDel("Hello, world!");

logDel -= ConsoleLogger.Log;
logDel += FileLogger.Log;

logDel("Hello, world, but from File!");

Enter fullscreen mode Exit fullscreen mode

Here's a summary of the changes made:

  • Removes the reference to the ConsoleLogger.Log method using logDel -= ConsoleLogger.Log.
  • Adds a reference to the FileLogger.Log method using logDel += FileLogger.Log
  • Invokes logDel with a new message that says Hello, world, but from File!

Run the application now, and you should see a Hello, world! logged to the console, and the application should terminate. However, if you navigate to the project folder, go inside bin/debug/net8.0 (Assuming you're on .NET 8), you should see a file called log.txt that was created automatically, which would have the following content:

The content of log.txt after logging a message to it

Awesome! You saw how we can replace method references and assign a target method to a delegate instance in 2 different ways!

Delegates with Anonymous Methods 🎭

We've seen delegate instantiation with methods that are pre-defined like Sum, but sometimes, you want to instantiate a delegate instance with an inline-implementation. That's useful when the method is not going to be reused somewhere else, which means defining a named method will be just more code, or in some cases, the implementation is short and you want to keep it concise.

Using this new delegate declaration, we'll see how we can associate an anonymous method with a delegate instance.


Operation op = delegate(double a, double b)
{
    return a + b;
}

delegate double Operation(double a, double b);

Enter fullscreen mode Exit fullscreen mode

Invoking this delegate will add the two numbers together. However, there's a shorter way of doing it, because the language has evolved, we no longer need to use the delegate keyword, and also, we don't necessarily have to specify the types of the parameters, because the compiler is smart enough right now, and it knows the definition of the delegate and that the Operation delegate accepts 2 double parameters, with that, we can transform the previous assignment to this:


Operation op = (x, y) => x + y;

delegate double Operation(double x, double y);

Enter fullscreen mode Exit fullscreen mode

This does the same thing now, but it's much more concise. Now omitting the types names is not necessary, in cases where the parameters list is long with complex type names, it's a good idea to keep the names present for enhanced readability and understanding.

Multicast Delegates 🐙

Earlier we've seen how to invoke a method through a delegate, then we replaced the method reference and called a different method which does a totally different job (Logging to a file instead of the console window), but what if we wanted to do both with one shot only? Enter multicast delegates.

A multicast delegate is a delegate that references 2 or more methods, which it invokes in the order they were added in, that would come in handy because right now, instead of logging to the console, then replacing the logging outlet to a file, then maybe in the future to a database, and who knows what else, we can simply reference all the Log methods and have the delegate invoke them all when we want to log something. This is how we would do that in C#:


LogDelegate logDel = ConsoleLogger.Log;
logDel += FileLogger.Log;

logDel("Hello, world!"); // Logs Hello, world! to both the console and log.txt

Enter fullscreen mode Exit fullscreen mode

Now the code logs the message to the console first, then logs it to the log file, because as I just mentioned, the order in which the references were added determines the order in which they'll be invoked, so pay good attention to that in case the operations your application is going to perform requires the processing to happen in a certain order.

Conclusion ✅

We looked in this introductory post about delegates on some key concepts and discussed the delegate reference type in detail, we saw through some code samples to learn how we can declare a delegate, how to assign a target method to it in two different ways, and also how we can remove or add method references to a delegate object instance. Lastly, we briefly looked at delegates with anonymous methods for more concise delegate assignments, and multicast delegates which allowed us to reference multiple methods at once using a single delegate object instance.

That's about it for this post, in the next one in the series, we'll see how we can pass methods as arguments into other methods using what we've learned from this article, so stay tuned for that one.

Thanks For Reading!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .