Let’s see why delegates are excellent for decoupling and organising your C# projects!
This article is also available on Medium.
When working on large software projects, one thing you want to make sure of is that your various systems are properly designed so that they interact in a clever way. In particular, it’s always interesting to aim for high decoupling, because it allows you to better maintain and validate your codebase.
C# offers lots of tools to do this, among which delegates.
So, today, let’s see what delegates are and why they are useful! 🙂
What are delegates?
Because C# is a strongly-typed language, you might sometimes feel like you are limited, or like some design patterns would be a pain to write. For example, when I first started, I thought that defining callbacks and passing functions to other functions would be way more difficult than with other languages, such as Python.
But this is exactly why C# has a great tool: the delegates.
In short, delegates are a way of defining your own “function types” so that you can then pass instances of it around and therefore transfer functions throughout your codebase. This makes it easy to create callbacks, of course, but it’s also handy to trigger several pieces of logic with one event, or to replace behaviour without having to change the initial code too much (or even at all).
The whole idea is that a function’s “shape” is defined not by its name, but by its input parameters and optional return value. For example, we could say that the sum, the subtraction, the multiplication and the division all have the same “shape” because they each take in two numbers and return one value.
In other words, we can expect each of these functions to have very similar prototypes:
public int Add(int a, int b); public int Sub(int a, int b); public int Mult(int a, int b); public int Div(int a, int b);
This is pretty cool, because it means that, from a “grammatical” point of view, these functions are interchangeable: they fit in the same place in your “code sentences”, they have the same role (which is to eat up two
int numbers and spit out another
int). They obviously give different results, but they all work the same.
It’s just like comparing these two sentences:
I like apples. I like tomatoes.
Sure, the two sentences don’t convey the same meaning – it is not the same to say I like apples, and I like tomatoes, because “apples” and “tomatoes” refer to different objects. However, you see that we can switch “apples” for “tomatoes” and still get a grammatically correct sentence.
Delegates rely on this concept: you define the “shape” of the functions (its place in the sentence), so that you can leave the exact body implementation (the word’s meaning) for later.
A basic example
Let’s keep our four operators and see an example of how to implement those using C# delegates. What I want is to create a very (very) simple calculator that asks you for two ints, then the operator to apply, and prints out the result of the computation.
We’ll first code it without delegates, and then switch over to delegates to see why they’re powerful in this situation.
The naive way
Our app isn’t too complex. For this first version, we can actually predefine our four operations so that they’re ready to use when needed:
Then, we’ll simply read various inputs to know what operation to use and on which numbers, and we’ll print the result:
That works perfectly, but it’s not very elegant. In particular, if we ever want to add a new operation, we’ll need to declare the function and then add a branch to the
if statement. Things can get crowded very quickly…
The “delegate” way
Because we’re interested in the “shape” of these functions, we’ll obviously need to make use of their prototypes; but then, because we want to have a delegate, we also need the
Mixing the two is as simple as writing this:
From that point on, we have a new type,
Operator, that can be used to declare functions obeying this prototype (two
int inputs, one
int return value).
Note: even if it looks like we declared a variable here, we actually declared a type – the
Operator object cannot be used as is, it is still just a template for actual instances 😉
Since we already defined our
Div functions, we actually already have implementations of our
Operator delegate. This means that we can easily map operator names to the right piece of logic, and then call them with our input numbers in the main routine:
This makes it way easier to add any function with a matching prototypes to the list, and it also makes it easy to extract this to an external file or subpart of the codebase to properly encapsulate the info!
Bonus: handling invalid inputs/null delegates
If we want to be thorough, we should make sure that, if the user enters an operation name that doesn’t match any function, the program doesn’t just crash! To do this, we can think of several solutions:
- we can add a “fake” properly-prototyped function to go in our mapping table in case the operation name is invalid (with a special code)… but even if it works, this is kind of a weird way to handle it:
- a better solution would be to check the inputs in the main routine, before we process anything, and warn the user about the invalid input:
Calling multiple functions at once
Another cool thing we can do with delegates is link multiple callbacks. This is particularly interesting when you do some logging and you want to output the logs to multiple destinations (for example the console, a file, a database, etc.).
I don’t want to actually dive into all these types of connections, but we can easily simulate this by defining multiple console logging functions.
Then, to be able to “overlay” multiple callbacks, we need to use another built-in object, the
Action, that is very close to the delegate. We simply have to specify its input parameters types, and
Actions have no return value so this gives us the entire expected prototype.
Finally, the trick is to “add” the various callbacks to this
Action – this is very similar to what you do with events when you want to stack multiple callbacks.
This way, whenever we call our logging entry point, all registered logging functions will automatically run!
Quick note: taking advantage of generics
The example we just saw with
Actions is a good example of the power of generics.
As explained in another article I wrote a while back, generic methods and classes are a way of defining a logic blueprint that can then be “filled” with any given type. This can be particularly useful for delegates, because it allows you to quickly “auto-define” variants of a function, without having to actually write all of them down.
A very common example of such a situation is when you need to compare objects to sort a list of them. While C# built-in
Sort() handles primitive types perfectly, it obviously can’t handle your own custom types (be them structs or classes) out of the box. Generics let you define your own comparison functions.
Similarly, generics could be used to “boost our calculator” from before, for example to allow floating numbers in addition to integers.
One straight-forward but sub-optimal way to do this would be to:
- define overrides for our
Divfunctions that take
floatinputs and return
- define another delegate to handle these new prototypes
- in the main routine, check whether the input numbers can be treated as
floats or not
The problem is that this implies writing a lot of redundant code, which always runs the risk of creating inconsistencies. Of course, operations as simple as
÷ usually don’t get coded wrong… but if we were using something more complex, then we would need to duplicate the logic for
floats and we could make an error at many steps of the process.
Also, if we then need to handle more precise values and want to switch to
doubles, we’ll have to code yet a third series of functions, and a third delegate, and keep all of the implementations in sync. This clearly lacks scalability!
So the solution to this issue would be to use generics instead; we could define a new delegate with a generic type, and then associated generic operators:
Note: for a real project, you would probably need to check whether the given
T type complies to the proper rules… for example with interfaces 😉
Delegates are a powerful C# tool that allows us to play around with functions and create our own runnable types. It can also help trigger several callbacks at the same time…
I hope you liked this article — feel free to leave a comment and tell me if you have other ideas for C# posts… and as usual, thanks a lot for reading 🙂