Delegate, Action, Func, Lamdba expression - What the heck!?

10/06/2022

C# offers a lot of utility especially around the delegate topic. So let's see what exactly a delegate is and how the distinct types like delegate, Action, Func, Predicate, anonymous function, lambda expressions and MulticastDelegate behave. Before we dive into what the exact objects do, we have to know what they are used for.

It is very nice that we can pass objects around like int or string but sometimes we want to pass a method to a method. And that is all the magic in very short. If you are familiar with C/C++ they are very much comparable with function pointers. If you used LINQ you are using delegates all the time. Look at Where function. If you want to have all even numbers of a list you would do something like this. numbers.Where(i = > i % 2 == 0). The Where function itself does not know how to filter and let's you decide how to apply a filter. It is only interested in two things: a bool return value to know which elements to filter out and an object for you to build your filter logic.

delegate

The most important fact at the beginning and it is super important that you keep this in mind:

All of the above listed types and things are delegates! All of them, no exception! This is vital to understand. They only differ in usage and when they were introduced into the .NET world.

To understand a delegate we can have a look what a "natural" delegate is:

A person designated to act for or represent another or others; deputy; representative, as in a political convention.

Source

And the technical version is similar. The delegate represents someone to the outside world and the outside world only sees the delegate instead of the represented function. Another way of thinking about what a delegate is, is very much like abstract classes and abstract methods work. The give the frame but you are responsible (when you inherit) to fill that frame.

Let's have a look at the following example:

delegate void PrintText(string s);

We can see it has no body. It is just the pure defintion which we then later can use.

DoMath addTwo = new DoMath(AddTwo);
DoMath multiplyByTwo = new DoMath(MultiplyByTwo);

Console.Write(addTwo(10));
Console.Write(multiplyByTwo(10));

int AddTwo(int number) => number + 2;
int MultiplyByTwo(int number) => number * 2;

delegate int DoMath(int number);

We have a delegate and two functions which fulfill the structure of that very delegate and we can see as long as the signature matches we can assign any of the functions to the delegate. Everyone who would interact with the delegate would not know which concrete function is behind it.

When would I use a delegate?

  • You are using a very old .NET framework version (like 1.1) - I guess very unlikely
  • You have a complicated signature, which would be very hard to understand with the other mechanism like: Func<IEnumerable<IEnumerable<MyObject>>>, int>. A named delegate might be better here: delegate int ProcessBatches(IEnumberable<IEnumerable<MyObject>> chunks)

Multicast delegate

One special thing in .NET is that every delegate (also Action, Func and friends) is a multicast delegate. That means you can not only attach one function to a delegate but n functions (also 0 if you want). So basically you can chain multiple commands to a single one. See it in action:

var printDelegate = new PrintToConsole(PrintLowerCaps);
printDelegate += PrintUpperCase;
printDelegate("Hello World");

void PrintUpperCase(string text) => Console.WriteLine(text.ToUpper());
void PrintLowerCaps(string text) => Console.WriteLine(text.ToLower());

delegate void PrintToConsole(string text);

The output on the console:

hello world
HELLO WORLD

Now one special thing is if your delegate returns a value, what will happen here? Its simple: The last chained method returns its value.

var printDelegate = new ReturnANumber(ReturnFive);
printDelegate += ReturnFour;
Console.Write(printDelegate());

int ReturnFive() => 5;
int ReturnFour() => 4;

delegate int ReturnANumber();

This will simply print 4 even though both functions got executed. Important thing is, that this applies to all delegates even though you might not use it that often.

Action

Actions were added with the .NET Framework 2.0 and as said in the beginning there are nothing more than a delegate. In this case a special delegate. An Action is defined that it has no return value. There are multiple overloads which correspond to the amount of parameters an action can have.

Action<string> printToConsole = text => Console.WriteLine(text);

Why do we have functions? What do they solve?

The first thing is: It is convenient. Look at the following example:

// Use Action
public void Print(Action<string> printAction) => printAction("Hello World");

// Use delegate
public delegate void PrintAction(string text);
public void Print(PrintAction printAction) => printAction("Hello World"); 

There is so much overhead when using delegates directly. You always have to define it somewhere instead of just the signature you expect.

The second thing is that delegates have some weird quirks and shortcomings. Look at the following example:

// Delegates: same signature but different types
public delegate void Foo();
public delegate void Bar();

// Consumer function -- note it accepts a Foo
public void Consumer(Foo f) {}

Consumer(new Foo(delegate() {})); // works fine
Consumer(new Bar(delegate() {})); // error: cannot convert "Bar" to "Foo"

There is no reason why this should not work. The structure is the same and from a technical point of view that should work, but it does not! But doing the same with an Action works without problem.

Action<string> printToConsole = PrintToConsole;
Action<string> anotherAction = PrintToConsole;

Console.WriteLine(printToConsole == anotherAction);

void PrintToConsole(string s) { } 

Just returns true. Super easy.

One thing one has to be aware is that Action does not go well with async and await. The reason is simple, an Action can't return anything so it behaves like a void function. Technically you would do async void. So no chance to await the task:

Action doWait = async () => await Task.Delay(2000);

// async void - fire and forget
// We will not wait 2 seconds we just go right through
doWait();

// Compiler error: Type 'void' is not awaitable
await doWait();

Func

Func, introduced with .NET Framework 3.5, is nothing special. It is also a delegate and behaves much like an Action. The only difference is that a Func has to return a value. And the return type is always the last defined generic parameter:

Func<int, int> MultiplyByTwo = n => n * 2;
Func<string, string, int> CountLengthOfBothStrings = (s, s1) => s.Length + s1.Length;

int number = MultiplyByTwo(10);
int length = CountLengthOfBothStrings("Hello", "World");

In contrast to Action Func does work well with async and await as you can return the Task object. Taken our example from above we can easily make it awaitable:

Func<Task> doWait = async () => await Task.Delay(2000);

await doWait();

Predicate

Predicate is just a special delegate. In its core it is defined as this:

delegate bool Predicate<in T>(T obj);

Functions like List<T>.Exists use this special delegate. The method signature could be easily the same as every LINQ query: Exists(Func<T, bool> predicate) but for backwards compatibility they did not change it. Remember the delegate keyword is in the Framework since version 1.1, Func since version 3.5. List<T> was introduced with version 2.0 so there was nothing else around at the time.

Anonymous function

Available since C# 2.0 the anonymous function is an inline delegate. We saw earlier in the delegate section that we always defined a "real" method to which the delegate was linked to. But we can also do something like this:

DoMath addTwo = delegate(int number)
{
    return number + 2;
};

Console.WriteLine(addTwo(2));

delegate int DoMath(int number);

This would of course print 4. What should be clear here as well is that this is still a regular delegate. So all the stuff which applied to delegates applies to anonymous functions as well.

Lambda expression

Lambda expressions, introduced with C# 7, are just a very convenient way of writing inline delegates. They are easily identifiable with the arrow notation:

Func<int, int> addTwo = n => n + 2;
Action sayHello = () => Console.WriteLine("Hello");

Console.WriteLine(addTwo(2));

Under the hood they are the same as anonymous functions. You can check all this stuff on sharplab.io.

Closure

Closures means that you bind a variable from outside scope of the function to the inside scope. Let's have a small look:

var someNumber = 100;

// We are binding "someNumber" inside our lambda expression
Func<int, int> addSomeNumber = n => n + someNumber;
Console.WriteLine(addSomeNumber(2));

So everytime you use an object inside your lambda expression/anonymous function/delegate, which is declared outside, the compiler has to close over it. It does that via some "magic". Behind the scenes it will generate a whole new class which captures your object. Why is this important? Well for multitude of reasons.

First:

var someNumber = 100;
Func<int, int> addSomeNumber = n => n + someNumber;
someNumber = 101;

Console.WriteLine(addSomeNumber(2));

The result is 103. If we would just save the variable in the time where the lambda was defined we would print out 102, but we keep somehow the reference to that object.

Second. Imagine you define a function where your closed variable is not available anymore. This still has to work:

// DoMath() returns us the Func, which we have to invoke again
// Therefore we have ()()
// Alternatively you can use DoMath().Invoke()
Console.WriteLine(DoMath()());

Func<int> DoMath()
{
    int someNubmer = 12;
    return new Func<int>(() => someNubmer * 2);
}

How does C# do that?

Taken the example from above:

var someNumber = 100;
Func<int, int> addSomeNumber = n => n + someNumber;
someNumber = 101;

Console.WriteLine(addSomeNumber(2));

It will create this:

internal static class <Program>$
{
    // This is our anonymous function
    private sealed class <>c__DisplayClass0_0
    {
        // The integer gets captured
        // Remember: Value Types on reference types get boxed so that we keep the reference
        // Therefore if the "real" someNumbes changes, this object is changed as well
        public int someNumber;

        internal int <<Main>$>b__0(int n)
        {
            return n + someNumber;
        }
    }

    private static void <Main>$(string[] args)
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.someNumber = 100;
        Func<int, int> func = new Func<int, int>(<>c__DisplayClass0_.<<Main>$>b__0);
        // The compiler uses our class field instead of the variable
        <>c__DisplayClass0_.someNumber = 101;
        Console.WriteLine(func(2));
    }
}

More in detail can be seen on sharplab.io.

Two things which are very important to understand when using closures:

  1. Value types do get boxed and put on the managed heap. I will put a link into the resources for me information about that.
  2. If you referencing big objects (or this) you will keep references around which could mean that the Garbage Collector is not able to remove your object. Just because it is living in a closure. Also for that a link at the end.

Conclusion

I hope I could give you a good overview what a delegate is and how it behaves in its different forms and shapes in .NET.

Resources

8
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x