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);
}
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);
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);
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);
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);
We can also remove methods from a delegate to do that we need to utilise -= operator
coolDelegate -= PrintNbPlusTen;
As well with delegates we can utilise anonymous functions
coolDelegate += (x) => System.Console.WriteLine(x+100);
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
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);
}
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
}
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");
}
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}");
}
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}");
}
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;
}
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;
}
Utilising Func in an annonymous functions
Func<int, int, int> multiFunc = (x, y) => x * y;
Console.WriteLine(multiFunc(2,4));
Now lets see it in a real case scenarios
Lets create a new console application
dotnet new console -n GradesApplication
And now let us create a class library which we will use to store our models
dotnet new classlib -n GradingLib
Now let us add reference to GradesApplication from GradingLib
dotnet add demo3/demo3.csproj reference GradingLib/GradingLib.csproj
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; }
}
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;
}
}
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
});
}
}
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;
}
}
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
});
}
}
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;
}
}
}
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);
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;
}
}
}
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;
}
}
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);
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.
Top comments (2)
Great explanation and very useful
Thank you
Very good explanation. Thank you.