DEV Community

Cover image for C# 9 - Delegate, Action and Func
Mohamad Lawand
Mohamad Lawand

Posted on

C# 9 - Delegate, Action and Func

In this article we will be discussing C# delegates. On this episode we will explore and dig deep into delegates, what are they and how do they work. We will explore Actions and Func and we will see how we can use delegates in a real life scenarios.

You can watch the full video on YouTube

And you can get the full source code from GitHub:
https://github.com/mohamadlawand087/v40-Delegates

So what we will cover today:

  • Ingredients
  • How to build Azure Functions
  • Code

As always please comment your questions, clarifications and suggestions in the comments down below. Please like, share and subscribe if you like the video. It will really help the channel

Ingredients

  • VS Code
  • Dotnet SDK

Delegate are defined outside of classes, they are their own entity. When i want to create a delegate i can create the same way i create a class.

We can create the delegate in the same file as our project outside the class, but based on the .Net documentations delegate should be created seperatly in a seperate file. And based on personal experience its always better to use delegates with class libraries we will discuss the reasonning behind it in our real world scenario.

So what exactly is a delegate?

In order for use to understand delegates let us see how normal variables work.

static void Main(string[] args)
{
    var nb1 = 3;
    PrintNumber(nb1);
    DoubleTheNumber(nb1);

    var nb2 = nb1 * 5;
    PrintNumber(nb2);
    DoubleTheNumber(nb2);

    var myName = "Mohamad";
    Console.WriteLine(myName);
}

public static void PrintNumber(int nb)
{
    Console.WriteLine(nb);
}

public static void DoubleTheNumber(int nb)
{
    nb *= 2;
    Console.WriteLine(nb);
}

public static void PrintMyName(string name)
{
    Console.WriteLine(name);
}
Enter fullscreen mode Exit fullscreen mode

We can see with variables we can assign them to different variables, we can pass them to methods and we can utilise extension methods to do some work on these variables. Delegates offers us the same flexibility but for methods, i know this might sound a bit confusing but let us delve deep and understand how we can use it and why we want to use it.

Delegates is to store methods as variables.

When i create a delegate i need to provide a signature of what type of method i need to store inside that delegate. We can think about it as an interface, its like a contract which we need to follow so we can use it.

public delegate void FirstDelegate(int number);
Enter fullscreen mode Exit fullscreen mode

Let us analyse this initialisation:

  • public: we are making this delegate accessible
  • delegate: we are creating a new delegate function
  • void: is the return type of our delegate any method that will utilise this delegate MUST have a void return type
  • FirstDelegate: we are assigning a name to our delegate
  • int number: is the parameter for the delegate, which means any method that will utilise this delegate must have an in parameter.

So what does that mean, it means that i will be able to store methods which have the same signatures (parameters and return type) inside this delegate.

So how can we can utilise the methods in the delegate, it is very simple we initialise the delegate in the same way we initialise a class

var firstDeleg = new FirstDelegate(DoubleTheNumber);
firstDeleg(4);
Enter fullscreen mode Exit fullscreen mode

We pass to the delegate the method that we want to store as a variable. And if i want to execute the method inside the delegate all i need is just pass the parameter i want and the delegate will do the work.

So far it seems its no different why would i need to do this if i can just call the method directly, the power of delegate comes in passing the methods around our code. Let us see the below example and see how it is being utilised

public static void DelegateHandler(FirstDelegate deleg)
{
    deleg(6);
}

DelegateHandler(firstDeleg);
Enter fullscreen mode Exit fullscreen mode

The cool this about this is we can see we are passing a complete method to another method and then utilising it, this is the power or delegate.

This will allow us to create abstract calls just by using delegates and avoid using if else statement which will allow us to follow the SOLID principles better

Delegates are typed checked, they are strongly typed

In a delegate we can store multiple methods, its not a single method so when i invoke a delegate each one of these methods will be invoked in the same order they were added to the delegate

We can utilise the += operator to add more methods to a delegate, we can add as many methods that we want as long as we have the same parameter and return types for the method.

public static void PrintNb(int x)
{
        Console.WriteLine(x);
}

public static void PrintNbPlusTen(int x)
{
        Console.WriteLine(x + 10);
}

public static void PrintNbPlusFive(int x)
{
        Console.WriteLine(x + 5);
}

public static void PrintNbPlusThree(int x)
{
        Console.WriteLine(x + 3);
}

// create a delegate
public delegate void CoolDelegate(int number);

// initialise a delegate
var coolDeleg = new CoolDelegate(PrintNb);
coolDeleg += PrintNbPlusTen;
coolDeleg += PrintNbPlusFive;
coolDeleg += PrintNbPlusThree;

// To invoke the delegate with all of the methods all we need to do is
coolDeleg(50);
Enter fullscreen mode Exit fullscreen mode

We can also remove methods from a delegate to do that we need to utilise -= operator

coolDelegate -= PrintNbPlusTen;
Enter fullscreen mode Exit fullscreen mode

As well with delegates we can utilise anonymous functions

coolDelegate += (x) => System.Console.WriteLine(x+100);
Enter fullscreen mode Exit fullscreen mode

The down side of using a lambda function or an anonymous function inside a delegate is that we canno remove them from the delegate.

Another way we can invole delegate is as follow

// These to will do the exact same thing
coolDeleg(50);
coolDeleg?.Invoke(50); // this will provide null checking so make sure our delegate is null safe
Enter fullscreen mode Exit fullscreen mode

There is also DynamicInvoke which is slower then the previous 2 but it is used when we are not sure about the delegate we are using.

public static void DynamicDelegateProcess(Delegate deleg)
{
    deleg?.DynamicInvoke(2);
}
Enter fullscreen mode Exit fullscreen mode

So what is happening in this method:

  • First we are utilising the base delegate class instead of specifying the actual delegate we want to use as we are not sure which delegate we are trying to pass
  • We cannot utilise the Invoke directly as we are not sure about the delegate type so we need to use DynamicInvoke
  • DynamicInvoke will try to pass the parameters we are passing to it to a delegate type which shares the same signature. Since this is not know during compile time the DynamicInvoke will accept any paramters that we pass to it and if there is any issue it will fail at run time
  • DynamicInvoke is much slower then Invoke only use it when not sure about the passed delegate
  • As much as possible try to make the code strongly typed so there wont be any need to use DynamicInvoke
DynamicDelegateProcess(firstDeleg);

public static void DynamicDelegateProcess(Delegate deleg)
{
    deleg?.DynamicInvoke(2);
    deleg?.DynamicInvoke("2"); // Fails
    deleg?.DynamicInvoke(); // Fails
    deleg?.DynamicInvoke(3,4); // Fails
    deleg?.DynamicInvoke("test", 3); // Fails
}
Enter fullscreen mode Exit fullscreen mode

Action & Func

Actions are built in delegates which allows us to have void methods and up to 16 parameters

When ever we want to use a delegate without a return type we should use actions

// the action in this case is essentially a delegate representing a void method
Action action = testMethod;
action += () => Console.WriteLine("Test 2");

action();

public static void testMethod()
{
    Console.WriteLine("Test 1");
}
Enter fullscreen mode Exit fullscreen mode

If we want our methods to have parameters we should use the generic version of the actions

Action<int> action = testMethod;
action += x => Console.WriteLine($"Action Test {x}");
action(10); 

public static void testMethod(int nb)
{
    Console.WriteLine($"Test {nb}");
}
Enter fullscreen mode Exit fullscreen mode

More advance case

Action<int, int, bool> action = testMethod;
action += (x, y, z) => Console.WriteLine($"Action Test {x}");
action(10); 

public static void testMethod(int x, int y, bool active)
{
    Console.WriteLine($"Test {nb}");
}
Enter fullscreen mode Exit fullscreen mode

Func is almost the same as an action but the main difference is that it has a return type, a none void method.

In order to use func they must have the following rules, the last generic parameter is the return type we want to save.

Func<int> func = GiveAnInt;
func += () => 43;

Console.WriteLine(func());

public static int GiveAnInt()
{
   return 40;
}
Enter fullscreen mode Exit fullscreen mode

if we call multiple methods inside the func it will only return the last value, it will execute all of the methods but the return will be from only the last method called.

Now lets see how we can add parameters to the func, its an easy implementaion


Func<int, string, int> func = GiveAnInt;

Console.WriteLine(func(1, "hello"));

public static int GiveAnInt(int x, string data)
{
     Console.WriteLine($"This is the data provided: {data}");
   return x;
}
Enter fullscreen mode Exit fullscreen mode

Utilising Func in an annonymous functions

Func<int, int, int> multiFunc = (x, y) => x * y;

Console.WriteLine(multiFunc(2,4));
Enter fullscreen mode Exit fullscreen mode

Now lets see it in a real case scenarios

Lets create a new console application

dotnet new console -n GradesApplication
Enter fullscreen mode Exit fullscreen mode

And now let us create a class library which we will use to store our models

dotnet new classlib -n GradingLib
Enter fullscreen mode Exit fullscreen mode

Now let us add reference to GradesApplication from GradingLib

dotnet add demo3/demo3.csproj reference GradingLib/GradingLib.csproj
Enter fullscreen mode Exit fullscreen mode

Now let us open our code in visual studio code.

The first thing we need to do is add a folder in the root of GradingLib classlib and call it Models. Inside the Models folder let us create our first Model Student and add the following

public class Student
{
    public string Name { get; set; }
    public double Grade { get; set; }
        public bool HasPassed { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now let us create another Model called Exam

public class Exam
{

    public List<Student> Students { get; set; } = new List<Student>();

    public double CalculateAvarageGrade(TopGrade topGrade)
    {
        return Students.Sum(x => x.Grade) / Students.Count;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now inside the Program.cs in our GradingApplication let us add the following

class Program
{
    static Exam exam = new Exam();
    static void Main(string[] args)
    {
        AddStudents();

        Console.WriteLine($"Students Avarage: {exam.CalculateAvarageGrade()}");
    }

    private static void AddStudents()
    {
        exam.Students.Add(new Student(){
            Name = "John",
            Grade = 70
        });

        exam.Students.Add(new Student(){
            Name = "Mohamad",
            Grade = 80
        });

        exam.Students.Add(new Student(){
            Name = "Donald",
            Grade = 90
        });

        exam.Students.Add(new Student(){
            Name = "Mickey",
            Grade = 50
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let us implement some delegate inside our application, we want to get the highest grade while we do the average grade calculation and to accomplish this we will use Delegates. lets navigate to Exam.cs inside the Models folder and add the following

public class Exam
{
    public delegate void TopGrade(double grade);
    public List<Student> Students { get; set; } = new List<Student>();

    public double CalculateAvarageGrade(TopGrade topGrade)
    {
        topGrade(Students.Max(x => x.Grade));
        return Students.Sum(x => x.Grade) / Students.Count;
    }
}
Enter fullscreen mode Exit fullscreen mode

And let us update our program.cs so we can utilise the delegate

class Program
{
    static Exam exam = new Exam();
    static void Main(string[] args)
    {
        AddStudents();

        Console.WriteLine($"Students Avarage: {exam.CalculateAvarageGrade(HighGrade)}");
    }

    private static void HighGrade(double grade)
    {
        Console.WriteLine($"High grade: {grade}");
    }

    private static void AddStudents()
    {
        exam.Students.Add(new Student(){
            Name = "John",
            Grade = 70
        });

        exam.Students.Add(new Student(){
            Name = "Mohamad",
            Grade = 80
        });

        exam.Students.Add(new Student(){
            Name = "Donald",
            Grade = 90
        });

        exam.Students.Add(new Student(){
            Name = "Mickey",
            Grade = 50
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Now if we run our application we can see that we get our grades as expected the avarage and the highest grade

The next step we will add a new delegate which will allow us to see how we can make our code more extensible. We will inject functionality to our classlibrary without the need to update it using delegates.

Will start by updating the Exam.cs Model we will add a new method to allow us to see if the student has passed or not based on certain conditions we will nee to add the following

public void PassStudents()  
{  
    foreach (Student std in Students)  
    {  
        if(std.Grade>=70)
        {  
            std.HasPassed=true;
        }  
        else
        {
            std.HasPassed=false;
        }
    }  
}
Enter fullscreen mode Exit fullscreen mode

We can see here only the students who got 70 or above has passed. but there one thing that which is not quite good about this what happens if we want to change the passing condition from 70 to 65 or to 75?

We will need to update the hard coded code and build our application again and redeploy it. This is not the ideal scenario One of best scenarios is to implement delegates to fix these issues.

Let us first start by adding a delegate in a new file called PassingCondition

public delegate bool PassingCondition(double grade);
Enter fullscreen mode Exit fullscreen mode

This delegate will contain the conditions that we want to check to check weather the students has passed or failed.

Next we need to update our passStudents method to accept delegates as follow:

public void PassStudents(PassingCondition isPassed)  // Passed the delegate
{  
    foreach (Student  std in Students)  
    {  
        if(isPassed(std.Grade)) // Passing the delegate parameters
        {  
            std.HasPassed=true;
        }  
        else
        {
            std.HasPassed=false;
        }
    }  
}
Enter fullscreen mode Exit fullscreen mode

Now inside our Program.cs inside our GradingApplication we need to add the following

public static bool StdPassing(double grade)
{
    if(grade >= 70)
    {
        return true;
    }
    else
    {
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

A method that will have the grading criteria.

And the way we call the exam.PassStudents to the following

var pCondition = new PassingCondition(StdPassing);

exam.PassStudents(pCondition);
Enter fullscreen mode Exit fullscreen mode

Now we can run the application and we can see that we are getting the expected results. Now lets say we want to update the passing grade from 70 to 75 we wont need to update the classlibrary all we need to is update our conditions in the console app and it should work.

Oldest comments (1)

Collapse
 
artydev profile image
artydev

Great explanation and very useful
Thank you