If you search for definitions of the word "delegate", you might find phrases such as “transferring power to someone” or “assign a task to a person”.
Those phrases describe very well what delegates in .NET are about. In
short they provide an ability to pass a reference to a method to some
code that can then invoke that method.
The purpose of this article is to demystify delegates and help you
gain some understanding about how different ways of using delegates
relate to each other.
Let’s start with a simple sample of delegate usage:
class Program
{
static void Main(string[] args)
{
PerformMathOperation(Add);
PerformMathOperation(Subtract);
}
public static void PerformMathOperation(Func<int, int, int> func)
{
int result = func(5, 4);
Console.WriteLine(result);
}
public static int Add(int x, int y)
{
return x + y;
}
public static int Subtract(int x, int y)
{
return x - y;
}
}
PerformMathOperation
. It takes one parameter of the type Func<int, int, int>
. This is a generic delegate that takes two int
values as parameters, and that returns an int
value. This means that this delegate can represent any method
fulfilling that signature. Note how the name of the method is completely
irrelevant from the delegate perspective. All that matters is the
signature, and the signature is the combination of the type of the
return value, the number of parameters and their types.
In the code sample above, I have left out the creation of the actual
delegate object; this is taken care of automatically by the C#
compiler. The statement
PerformMathOperation(Add)
is identical to
PerformMathOperation(new Func<int, int, int>(Add))
, only shorter and –
in my opinion – easier to read.
In the main method, I call
PerformMathOperation
twice. The first time I pass a delegate referencing the Add
method, and the second time I pass a delegate referencing the Subtract
method. Inside PerformMathOperation
the code will invoke whatever method the delegate is referencing,
collect the result and print it to the console. The output will look
like this:9
1
We can clearly see how the program has first called
Add
, and then Subtract
.
This is in a nutshell what delegates are all about; instead of passing a
value you can pass a method that will carry out some action when
invoked and optionally return a value.
In the color coded code above, I have inserted some highlights to
clarify some elements of the code. Orange is where delegates are
created, getting a reference to a method. Blue is where the delegate is
invoked (effectively executing the method it is referencing). Purple is
the actual method body of the methods being called through delegates,
and yellow are the parameters for those methods.
As you can see, a method is not executed when becoming referenced by a
delegate, but when the delegate is invoked. This is important to keep
in mind: delegates are just an object carrying a reference to a method
that may be invoked later. As such they can also be stored in variables
for later use:
Func<int, int, int> dlgt = Add;
// ... and then later on:
int result = dlgt(5, 4); // result will be 9
As I mentioned, the delegates can be created by writing the full form
(new Func ...) or a shorter form, leaving some of the work to the
compiler. This has no effect of any kind on the resulting code. The same
goes for invoking the delegate. In the previous code sample, the
delegate variable is used as if it was a method like any other method.
Again, there is some compiler magic behind the curtains. In fact, the
following three code blocks will result in the exact same code coming
out from the compiler:
// first
Func<int, int, int> dlgt = Add;
int result = dlgt(5, 4);
// second
Func<int, int, int> dlgt = new Func<int, int, int>(Add);
int result = dlgt(5, 4);
// third
Func<int, int, int> dlgt = new Func<int, int, int>(Add);
int result = dlgt.Invoke(5, 4);
Which one of these you choose to use is entirely a matter of style.
Lambdas an anonymous methods
One of the most common places where you get in touch with delegates
are when using LINQ. The following code sample illustrates a common
use-case:
static void Main(string[] args)
{
IEnumerable<string> inputData = new[]
{
"Jimi Hendrix",
"Janis Joplin",
"Curt Cobain"
};
IEnumerable<string> result = inputData.Where(s => s.Contains("x"));
foreach (var item in result)
{
Console.WriteLine(item);
}
}
I have a sequence of strings, called
inputData
and want to filter out all strings containing the letter "x", so I use the LINQ extension Where
to do this:inputData.Where(s => s.Contains("x"));
Let's look at a color-coded version of that statement to compare it to the delegate usage samples above:
Again, orange is where the delegate is created, purple is the method
body of the code being executed and yellow are the parameters. In
contrast to the previous samples, we use an anonymous method, so the
delegate creation and method declaration happens in the same place, but
the elements are still the same.
Somewhat simplified we can say that whatever appears on the left-hand side of
=>
is equivalent to the parameters of a method declaration (whatever
appears within the paranthesis), while whatever appears on the
right-hand side of =>
is the method body.
Also, just as in previous examples the C# compiler takes care of some
of the "heavy lifting", figuring some things out for so I don't have to
write them. I'll get back to that later.
If you check how the
Where
method is declared, it looks like this:public static IEnumerable<tsource> Where<tsource>(
this IEnumerable<tsource> source,
Func<tsource , bool> predicate
)
As you can see, this is an extension method for
IEnumerable<T>
(that is revealed by the fact that the method is static
, and that the first parameter is declared with the this
keyword), so it can be called for any IEnumerable<T>
, such as our IEnumerable<string> inputData
.
The second parameter is a
Func<tsource, bool>
. This is a delegate type, that refers a method taking one parameter of type tsource
, and that returns a bool
.
Like with "invisible" delegate declaration above, the C# compiler
kindly helps out here as well. Looking at the declaration of the
Where
extension method, you can see that it has a generic type argument: Where<tsource>
.
The type argument is inferred by the compiler, so you typically do not need to type it into your code.
The statement
inputData.Where(s => s.Contains("x"));
is identical to inputData.Where<string>(s => s.Contains("x"));
but since inputData
is a sequence of strings, the compiler can figure out what type argument to use.
In our example (which is the typical way of writing a simple lambda
expression when using LINQ), we have left it up to the compiler to fill
in quite a few blanks. If we were to provide the full code, and without
the lambda operator, it would look like this instead:
inputData.Where(new Func<string, bool>(delegate(string s) { return s.Contains("x"); }));
While this looks drastically different, there are elements that we
can recognize: the parameter s in the method declaration, the
s.Contains("x")
in the method body. In this code sample, they play exactly the same role
as they do when constructing the code using the lambda operator.
Let's transform this somewhat chatty code back to the very short
lamdba expression, step by step. First let's use the lambda operator to
remove the explicit delegate declaration:
inputData.Where((string s) => { return s.Contains("x"); }));
Now it becomes evident that what we do is to essentially remove all
code that is about declaring the delegate, but we keep the parameter
declaration and the method body, separated by the lambda operator.
Next, we can let the compiler infer the type of the input parameter:
inputData.Where((s) => { return s.Contains("x"); });
Since we operate on a list of strings, the input to the method must
obviously be of the type string. Next, since the method body consists of
only one line, we can remove the curly braces.
Also, when destilling the method body in the lambda expression down
to a one-liner, we can (and even must) remove the return keyword:
inputData.Where((s) => s.Contains("x"));
Finally, since the number of parameters to the method is one, we can remove the paranthesis around it:
inputData.Where(s => s.Contains("x"));
All these code snippets result in the same code.
As you can see, there are a few different ways you can write code
that involves declaring and using delegates, but they all end up
rendering almost the exact same kind of code in your application, and
there is no magic going on, just a helpful compiler making some things
easier.
Comments
Post a Comment