Delegates
The Crash Course
- Delegates are type definitions that make it possible to pass methods around as though they were data.
- This provides a way to change what code is called in much the same way that you can change a variable.
- 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 assumes that you've got an Add method that returns a float and takes two floats as parameters to match the needs of the delegate.
The Problem Delegates Solve
Consider this method that adds one to every entry in an int[]:
static void ModifyArrayByAddingOne(int[] numbers) { for (int index = 0; index < numbers.Length; index++) { numbers[index] = numbers[index] + 1; // Or numbers[index]++; } }
This code seem fine as it is, but let's suppose you decided you wanted to be able to add any number to the elements in an array. We'd modify it to something like this:
static void ModifyArrayByAddingAmount(int[] numbers, int amount) { for (int index = 0; index < numbers.Length; index++) { numbers[index] = numbers[index] + amount; } }
But what if we wanted to also be able to double all the numbers in the array? We probably can't reasonably just tack this on to the method above, but we could create another method that is very similar:
static void ModifyArrayByDoubling(int[] numbers) { for (int index = 0; index < numbers.Length; index++) { numbers[index] = numbers[index] * 2; } }
But what if we were thinking we might want a version that squares the numbers (multiplies it by itself)? Or divides it by three? Or triples, then adds one? We'd end up creating a whole lot of very similar methods.
Normally, when we see duplicated or very similar code, we want to start looking for ways to eliminate the duplication to make it easier to change. The most obvious tool to do that is by extracting a section of the code into another method. Unfortunately, in this case, the part that changes is the very beating heart of the code, right in the middle, and there probably isn't a method that we can easily make. The part that says the same is the iteration down the array. The part that changes is the way we compute a new value from the old value. That is the part we'd like to make flexible. And delegates will make that possible.
Defining a Delegate Type
The first thing we need to do is recognize what the parameters and return type of a method would look like that can do the section of our code that varies. In this case, we're taking an int value and performing some manipulation to it to create a new int value. So if we could make a method that does this manipulation, it would have an int return type and a single int parameter.
For example, we could make a method that simply adds one to an int value:
static int AddOne(int input) { return input + 1; }
Or we could make a method that doubles an int value:
static int Double(int input) { return input * 2; }
Or one that triples and adds one:
static int TripleAndAddOne(int input) { return input * 3 + 1; }
The goal is to find a pattern that works for the methods we might want to apply, and I think we've found it here.
Defining a Delegate Type
The next step is to define a new type to represent this pattern. Variables whose type is a delegate type will be able to hold references to methods, so a delegate type definition looks very much like a method. Capturing the pattern above in a delegate type is not too bad:
delegate int NumberDelegate(int input);
This is a type definition, so code like this needs to either go at the bottom of your Program.cs file with your enumeration, class, struct, and interface definitions, or it needs to go in its own file.
This defines a new type called NumberDelegate which can be used for a variable's type, including a local variable and a parameter.
Using a Delegate Type
Now we can go back to our ModifyArray methods and make a new method to rule them all and, in the darkness, bind them:
static void ModifyArray(int[] numbers, NumberDelegate desiredChange) { for (int index = 0; index < numbers.Length; index++) { numbers[index] = desiredChange(numbers[index]); } }
This version has a new parameter, whose type is NumberDelegate. Essentially, we expect this variable to point to a method. It will be actual code that can run, not just plain old data, as all of our other data types have been.
Down in the body of ModifyArray, we use desiredChange. If you look at how it is used, it is basically called exactly as though it were a method: desiredChange(numbers[index]). That's the magic of delegates. We can pass around code itself. ModifyArray has no clue what code it was just given. It just knows that you feed it an int and it will return an int. But that's all it needs to know.
Our method can now be used to perform any manipulation that we can squish into the format dictated by the NumberDelegate type.
Calling a Method with a Delegate Parameter Type
The last piece of the puzzle is figuring out how to call ModifyArray, now that it has a parameter with a delegate type.
This demands making a longer sample, so here it is:
int[] numbers = new int[] { 1, 2, 3, 4, 5 }; ModifyArray(numbers, AddOne); ModifyArray(numbers, Double); ModifyArrays(numbers, TripleAndAddOne); static int AddOne(int input) { return input + 1; } static int Double(int input) { return input * 2; } static int TripleAndAddOne(int input) { return input * 3 + 1; } static void ModifyArray(int[] numbers, NumberDelegate desiredChange) { for (int index = 0; index < numbers.Length; index++) { numbers[index] = desiredChange(numbers[index]); } } public delegate int NumberDelegate(int input);
The main part to notice is that when we call ModifyArray, we pass in the identifier of the method we want to use. But we do not use parentheses! That part is easy to do wrong.
By using parentheses, we are invoking the method and attempting to supply its return value to ModifyArray, which is not what we want to do here. Instead, we want to hand off a specific, named method to ModifyArray. We do that by giving it the name only, without invoking the method. So no parentheses!
Other Uses of Delegates
We're only seeing a simple example here. Delegates give us flexibility in very interesting and flexible ways. For example, you could imagine something similar to the above code, only we ask the user what they want to do, given some options. That code could be done in a method. But that method could return the chosen method. A method that returns other methods!
In fact, we'll see events in the next tutorial and events build upon delegates.
It may take some exploration before you start to see all the ways that you can use delegates to pass references to methods around. It is not the most intuitive of concepts. But it is an extremely powerful one for which you'll find plenty of uses in time.
What's Next?
Delegates can be a little confusing, but like with many of the things we've discussed, you don't necessarily need to know all the details about them immediately. 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.
Hi! Not sure how important this is, but I think I've found a typo:
"The part that says the same is the iteration down the array." - I think 'says' should be 'stays' here?
Also, you use the heading "Defining a Delegate Type" twice - is this intentional?
Thank you so much for these amazing tutorials. Keep up the great work! Or don't - you've done enough.
Post preview:
Close preview