Delegates

Delegates

The Crash Course

  • In the real world, a delegate is someone or something that does work for someone or something else.
  • In C#, delegates provide a framework for methods to be passed around like objects or other variables—effectively allowing you to "delegate" to various methods on the fly, as needed.
  • To create a delegate, you use the delegate keyword, like this: public delegate float MathDelegate(float a, float b); This delegate can now be used to keep track of any method with the same return type (float) and the same parameter list (float, float).
  • You can then use a delegate in a way that is very similar to a variable: MathDelegate mathOperation = Add; float a = 5; float b = 7; float result = mathOperation(a, b);. This, of course, implies that you've got an Add method that returns a float and takes two floats as parameters, to match the needs of the delegate.

Delegates in the Real World

C# aside, let's start by talking about what a delegate is, in general terms. The definition of a delegate is someone or something that does work or fulfills some responsibility in behalf of another person or thing.

You've maybe heard the word "delegate", or "delegation" (meaning a group of delegates) in the political or government world. Many modern governments have a system where people vote or otherwise choose one person or a group of people to do certain work for them. These people are idiot politicians--err… wait… did I just say that? I meant, these people are representatives, or delegates of the people who chose them, and it is their responsibility to accomplish a certain task, in whatever way they want to do it, in behalf of the people who picked them.

Delegates in C#

Back in the C# world, we have methods, which are simply blocks of code that accomplish particular tasks. They do their task in their own way, and from the outside world, we don't care about how it is done, just that it gets done. Now, not all methods do the same kind of work. Some print stuff for the user to see, some to math, and, of course, some just make the program crash.

C# has a concept in the language called delegates, like in the real world. Now, I'll admit, this can be a bit of a tricky subject to really grasp. I've rewritten this tutorial about four times from scratch, coming up with a useful way of explaining this.

A delegate in C# basically says "I've got some work that needs to be done, and I want to hand it off to someone else who can make this happen." Delegates will indicate exactly what kind of information they are going to hand off (the work they want done) as well as what they expect to get back.

This is done by essentially stating that any method that wants to do this work needs to have certain parameters, and must return a certain type. On the other hand, unlike an interface, we don't care about what the method is actually called. Just the list of parameters and the return type.

Declaring a Delegate

Having laid out the background, it is time to take a look at how you'd actually create a delegate, which I think will also help explain how it works a little better.

Let's start off by creating a delegate that basically represents any method that can do some sort of math with two numbers. So, basically, what we'll be doing is saying "I've got a couple of numbers, and I want somebody--anybody--who can do some math with those two numbers and give me the result back." There might be some method out there that can add the two numbers together and return them, or there might be a method that takes them and multiplies them together. Or one that squares the two, adds them, and takes the square root. (By George! I think we've discovered the Pythagorean Theorem!) The point is, we're just on the lookout for any method in existence that can do something with two numbers to produce a result.

Now that we know what we're trying to do, let's make it so.

Delegates are typically created directly in the namespace, just like classes, structs, and interfaces. In fact, just like all of those, I'd recommend putting delegates in their own file to keep them separate.

To do this, go ahead and create a new .cs file, with the name of your delegate (MathDelegate.cs in this case). Adding the following single line of code to actually create the delegate:

public delegate float MathDelegate(float a, float b);

So our whole MathDelegate.cs file should look something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace Delegates
{
    public delegate float MathDelegate(float a, float b);
}

So here we've used the delegate keyword to say we're looking for any method that takes two floats and returns another float. This delegate is called MathDelegate (though of course, you can call it whatever you want).

Using Delegates

The first thing we will need in order to actually use delegates is some methods that match the needs of the delegate. Basically, we need some methods that take two floats as parameters, and return another float. So I've gone back to my main Program class, where the Main method is located, and created these three methods, all of which match the needs of the delegate:

public static float Add(float a, float b)
{
    return a + b;
}
 
public static float Subtract(float a, float b)
{
    return a - b;
}
 
public static float Power(float baseNumber, float exponent)
{
    return (float)Math.Pow(baseNumber, exponent);
}

Three methods, all of which take two floats as parameters and return a float, but each of which does their own thing with the numbers.

Perhaps, in a specific situation, you'll already have those methods defined long before you create a delegate for them, and if that is the case, you'd be able to skip this step.

Using the delegate is a bit like using an object. Back in my Main method, I've added code to use any one of these methods, going through the delegate. (This doesn't have to go in the Main method—you just put it where you need it.)

MathDelegate mathOperation = Add;
 
float a = 5;
float b = 7;
 
float result = mathOperation(a, b);

We see here that we create a new "variable" with the MathDelegate type, and give it a name, just like with any other variable. We then assign it a value—the name of one of the methods that match the demands of the delegate (two float parameters and a float return type in this case).

The next two lines, setting up a and b, should make sense by now. We're just doing some basic setup type stuff.

On the last line there, we use our delegate, which looks a whole lot like a method call. And in a sense it is a method call, we just go through the delegate. Since mathOperation is currently assigned to the Add method, the Add method is what gets called, and result, here, would now be 12.

Of course, we're not just stuck to creating a delegate and assigning it a value right away. We can do logic to figure out which of all of the possibilities we want to use:

bool subtractionIsMoreFunThanAddition = true;
 
MathDelegate mathOperation;
 
if(subtractionIsMoreFunThanAddition)
{
    mathOperation = Subtract;
}
else
{
    mathOperation = Add;
}
 
float a = 5;
float b = 7;
 
float result = mathOperation(a, b);

So here, depending on the state of the boolean variable isSubtractionMoreFunThanAddition, we may end up calling the Subtract method instead.

Methods as First-Class Citizens

One thing you may have noticed is how similar using delegates are to using objects or variables. This is by design.

Delegates allow you to assign methods like variables, pass methods in to other methods as a parameter, return methods from other methods.

In the world of programming languages, there's a word for this ability: delegates make it so methods are first-class citizens, like value typed variables (int, byte, float, etc.), objects, and structs.

This ability is actually extremely powerful if you know how to use it. In the next tutorial, we'll look at events, which are going to be extremely useful, especially if you head into the world of GUI programming and making user interfaces that are better than the console.

But even aside from events, delegates have a whole lot of power on their own, and so they are worth knowing about.

A More Sophisticated Example

In a final push to illustrate how delegates can be useful, I'm going to wrap up with a more powerful and, admittedly, somewhat more complex and confusing example. But I'll level with you. If you have more or less understood what we've done so far, at a basic level, and you're ready to move on to events, truly understanding this section isn't going to be critical. So you can just jump ahead to events now, if you really want.

So let me start off by describing where we're going to go with this more sophisticated example. Let's say you have an object, any old object, that you are using. And you have a List of those objects that you are working with, too. Let's say you've also got dozens of different operations that you want to do on each item in the list at different times.

For simplicity, let's assume that the object we're using is defined like this:

public class MagicNumber
{
    public int Number { get; set; }
 
    public bool IsMagic { get; set; }
}

It is a simple class, but I'm just trying to keep the example as simple as possible. You'll likely have way more complicated classes.

At any rate, we've also got some methods floating around that do stuff with MagicNumbers. Like, perhaps these:

public void Increment(MagicNumber magicNumber)
{
    magicNumber.Number++;
}
 
public void Decrement(MagicNumber magicNumber)
{
    magicNumber.Number--;
}
 
public void Magicify(MagicNumber magicNumber)
{
    magicNumber.IsMagic = true;
}
 
public void DeMagicify(MagicNumber magicNumber)
{
    magicNumber.IsMagic = false;
}

Again, I'm still trying to keep this simple. Any of these methods could do all sorts of things with a MagicNumber. That's fine.

But like we saw earlier, we can see that we've got all of these methods that all take a MagicNumber as input, and return nothing. So this is a prime opportunity for a delegate.

So we could create our delegate that looks like this:

public delegate void MagicNumberModifier(MagicNumber magicNumber);

Now, remember that in our main program, somewhere, we've got some sort of container for all of these MagicNumbers we've got floating around. For simplicity, we'll stick with a List, but it really could be anything.

So something like this:

List<MagicNumber> magicNumbers = new List<MagicNumber>();
 
magicNumbers.Add(new MagicNumber() { Number = 4, IsMagic = true });
magicNumbers.Add(new MagicNumber() { Number = 6, IsMagic = false });
magicNumbers.Add(new MagicNumber() { Number = 9, IsMagic = true });
magicNumbers.Add(new MagicNumber() { Number = 13, IsMagic = false });
magicNumbers.Add(new MagicNumber() { Number = 20, IsMagic = true });

Now, we can do something like this:

public static void ApplyModifierToAll(MagicNumberModifier modifier, List<MagicNumber> magicNumbers)
{
    foreach(MagicNumber number in magicNumbers)
    {
        modifier(number);
    }
}

Then in our Main method, or anywhere else, we can do this:

public static void Main(string[] args)
{
    List<MagicNumber> magicNumbers = new List<MagicNumber>();
 
    magicNumbers.Add(new MagicNumber() { Number = 4, IsMagic = true });
    magicNumbers.Add(new MagicNumber() { Number = 6, IsMagic = false });
    magicNumbers.Add(new MagicNumber() { Number = 9, IsMagic = true });
    magicNumbers.Add(new MagicNumber() { Number = 13, IsMagic = false });
    magicNumbers.Add(new MagicNumber() { Number = 20, IsMagic = true });
 
    ApplyModifierToAll(Increment, magicNumbers);
    ApplyModifierToAll(Magicify, magicNumbers);
}

This, by the way is a prime example of the Visitor design pattern. If you don't know what that is, well… don't worry. In reality, it's a discussion for another day. In short, though, a "design pattern" is a general solution to a set of similar problems. Not everyone uses design patterns. In fact, not everyone even supports them. The Visitor pattern, though, is one where you can do operations on a type of object without having to worry about the data storage structure (like the List we're using) that they're contained in.

What's Next?

Delegates can be a little confusing, but like with many of the things we've talked about, you don't necessarily need to know all of the details about them right away. A basic understanding should keep you going. But we'll definitely want that basic understanding as we move on to the next tutorial, because events rely heavily on delegates to do what they do.