Mastering Action and Func Delegates in C#: A Comprehensive Guide
Rahul Kumar Jha
Posted on January 7, 2023
In this article, we are going to learn about implementing Action and Func delegates in C#.
Delegates are an essential part of the internal implementations of a broader range of functionalities in the .Net ecosystem.
Action and Func are built-in delegates that the framework provides, easing the burden of cluttering the code base with similar type definitions.
They are an essential component of functional programming in C#.
We have heard the famous OOP proverb "Programming to an interface, not to an implementation" or "Programming to an abstraction, not to an implementation." That's exactly what delegates can help us with.
After understanding delegates in general, we will continue shaping our knowledge around Action and Func, solidifying our understanding with concrete examples of how to use them in .Net.
Let's get going.
What is a Delegate?
A delegate is a type-safe reference to methods defined in a class. But what is type-safety? And why is it important when it comes to delegates?
The delegate defines the exact type of parameters it accepts and the type it returns. Compatibility between the method and the delegate is possible if the method has the same signature as the delegate.
Let's define a delegate.
public delegate string[] Filter(string str);
The delegate Filter
takes a string
parameter and returns a string
array.
What about methods compatibility with Filter
delegate?
Let's take a look:
public static string[] SplitText(string str)
{
return str.Split(' ');
}
public static int TextLength(string str)
{
return str.Split(' ').Length;
}
The SplitText
method has the same signature as the Filter
delegate, but the TextLength
method does not match the Filter
argument type due to a dissimilar return type.
What happens when we assign TextLength
to the Filter
delegate? No surprises here:
Filter filter = TextLength; // Error: Cannot convert from 'method group' to 'Filter'
The compiler will issue an error when trying to assign TextLength
as a method reference to the Filter
delegate.
The type safety feature of delegates acts as a binding contract that methods must adhere to.
Why Action and Func?
Before the introduction of Generics in the .Net framework 2.0, a new delegate definition was required for varying arguments or return types.
Let's define a few delegates:
public delegate void Log(string message);
public delegate void TotalDiscount(decimal discount);
The Log
and TotalDiscount
delegates differ only in terms of the type of argument they expect. Isn't it better to have just one delegate definition for both of them?
public delegate void Action<T>(T arg);
The Action
delegate is compatible with any method that has no return type and accepts one argument of type T
.
What about a delegate with a return type other than void
?
public delegate string Filter(string phrase);
public delegate string[] Split(string phrase);
Not to worry, we can define a delegate using generics to match both the Filter
and Split
delegates:
public delegate TResult Func<T, TResult>(T arg);
The Func
delegate accepts an argument of type T
and returns type Tresult
. This matches any method definition that has one argument of type T
and returns a type other than void
.
We don't need to define similar delegates anymore. We can use generic Action
and Func
instead, correct?
Consider this: a delegate could take a varying number of parameters with or without a return type other than void.
Let's define a few generic delegates:
public delegate void Action<Tin1, Tin2>(Tin1 arg1, Tin2 arg2);
public delegate Tout Func<Tin1, Tin2, Tout>(Tin1 arg1, Tin2 arg2);
Action
and Func
take two arguments. Similarly, we could define them with as many arguments as we wish.
Thankfully, Action
and Func
delegates were made available in the .Net Framework version 3.5 under the System
namespace. In most cases, we don't need to define them ourselves.
The Action delegate has a return type of void
whereas the Func delegate has a return type other than void
. Both can take up to 16 parameters.
How Do We Declare/Initialize Action and Func?
The Action delegate have generic as well as non-generic definitions. We can declare a variable of a type Action that takes no argument or a more generic one as Action<T1>
, Action<T2>
, Action<T3>
all the way to Action<T1, T2, T3, ............, T16>
that accepts from 1 to 16 arguments.
Let’s look at a few inbuilt Action
declarations and initialization:
Action notify = () =>
{
Console.WriteLine("Actions are great.");
};
Action<int> doMath = (x) =>
{
Console.WriteLine($"The square of {x} is {x * x}");
};
Action<int, decimal> calculateDiscount = (x, y) =>
{
Console.WriteLine($"The discount is {x * y}");
};
The notify
, doMath
and calculateDiscount
variables of type Action
have varying lengths of arguments initialized with compatible inline method definitions.
The Func delegates are all generic as they always have a return type associated with them which is not the case with Action delegate that always returns void.
The Func<TResult>
delegate takes no argument and returns TResult. We can declare Func<T1, TResult>
, Func<T1, T2, TResult>
all the way to Func<T1, T2, T3, ........., T16, TResult>
that accepts from 1 to 16 arguments always returning the value of type TResult
.
Let’s see a few inbuilt Func
declarations and initialization:
Func<bool> isTrue = () => true;
Func<string, string[]> ToArray = (s) => s.Split(' ');
Func<string, int, string> Concat = (s, i) => s + i.ToString();
The last parameter in the Func
declaration denotes the return type of the delegate. The isTrue
has no argument, it returns a boolean
value. The ToArray
accepts a string and returns an array
of strings. The Concat
accepts two arguments of type int
and string
, and it returns a string
value.
Enough talking, let’s have a practical demonstration to understand Action and Func delegates better.
Action and Func as Callback Functions
Action and Func are special types of variables that hold methods as references.
The variables of type Action and Func can be used just like any other variables, but they can also be invoked like a method.
This can be useful when you need to pass a method as an argument to another method, or when you want to store a method in a variable for later use.
Let’s unravel it:
public static IEnumerable<int> ProcessList(IEnumerable<int> nums, Func<int, int> func)
{
foreach (var num in nums)
{
yield return func(num);
}
}
The ProcessList
method accepts two arguments, one of which is a func
delegate of type Func<int, int>
. The ProcessList
method has no idea of what method the func
delegate refers to, as long as the method passed to it adheres to the func
definition.
This is an example of how you can use the Func delegate to pass a method as an argument to another method. The ProcessList
method can then use the func
delegate to invoke the method passed to it.
Simple Callback Pattern Implementation With Action and Func
Having a working solution is the best thing. We will create a small application to experiment with the concept of callbacks using Action and Func.
We start with creating our model classes first:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Course> Courses { get; set; } = new List<Course>();
public static IEnumerable<Student> DummyStudents()
{
return new List<Student>
{
new Student { Id = 1, Name = "Rahul" },
new Student { Id = 2, Name = "Vikas" },
new Student { Id = 3, Name = "Neha" },
new Student { Id = 4, Name = "Abhi" },
new Student { Id = 5, Name = "Lara" }
};
}
}
The Student
class has an Id
and Name
property. Each Student has a Courses
collection. It also defines a static helper method called DummyStudents
which populates some dummy data for the Student
objects.
Next, we create the Course
class with an Id
, Name
, and Price
property. The Course class also contains the DummyCourses
static method that returns a list of courses
:
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public static IEnumerable<Course> DummyCourses()
{
return new List<Course>
{
new Course { Id = 1, Name = "C#", Price = 99.99m },
new Course { Id = 2, Name = "Java", Price = 149.99m },
new Course { Id = 3, Name = "Python", Price = 199.99m },
new Course { Id = 4, Name = "JavaScript", Price = 149.99m },
new Course { Id = 5, Name = "Ruby", Price = 199.99m },
new Course { Id = 6, Name = "PHP", Price = 99.99m }
};
}
}
Now let's create a StudentService
class. We want to declare a method to filter students that match a specific condition:
public static class StudentService
{
public static Student GetStudent(IEnumerable<Student> students, Func<Student, bool> predicate)
{
foreach (var student in students)
{
if (predicate(student))
{
return student;
}
}
return default;
}
}
The GetStudent()
method takes a list of students
and applies a special Func called predicate
, iterating over the list.
Next, we are going to use the GetStudent()
method of the StudentService
class to filter students and see the result in our Main
method inside our Program
class.
internal class Program
{
static void Main(string[] args)
{
var students = Student.DummyStudents();
var student = StudentService.GetStudent(students, (s) => s.Id == 3);
Print(student, (s) => Console.WriteLine($"Id: {s.Id}, Name: {s.Name}"));
}
public static void Print<T>(T arg, Action<T> printAction)
{
Console.WriteLine("Let's look at the output..");
printAction(arg);
}
}
Inside our Main
method, we first populate the dummy students. Then, we call the GetStudent()
method of the StudentService
class, passing in the students and an inline method that matches the Func<Student, bool>
delegate.
Looking at (s) => s.Id == 1
, we see that the lambda expression returns true if the id matches 1.
We call the Print
method to display the result, verifying that indeed we receive a student with an id of 1:
Let's look at the output..
Id: 1 Name: Rahul
We should not ignore the awesomeness of the Print
method. It can operate on any argument of type T
and call any action on it.
We can modify the display behavior by updating the inline function (s) => Console.WriteLine($"Id: {s.Id} Name: {s.Name}")
that we pass to the Print
method, matching the generic variable of type Action<T>
.
The dynamic nature of the Print
method allows it to operate on any argument of type T
and call any action on it, which means that it can save the result in memory instead of printing it on the console.
This demonstrates the concept of "programming to an abstraction, not to an implementation" which means that you should design your code to depend on abstractions (such as interfaces or delegates) rather than on specific implementations.
This allows you to change the implementation of a component without affecting the rest of the system.
One of the features of a delegate is the ability to hold references to more than one method. This is called multicasting, and it allows you to invoke multiple methods with a single delegate invocation.
This can be useful when you want to notify multiple listeners about an event, or when you want to perform multiple actions in response to a single event.
We are going to extend our solution to demonstrate how Action or Func can call more than one method.
Simple Pub/Sub Pattern Using Action
Let’s extend the StudentService
class to include the method EnrollStudentToCourse()
.
We want to send notifications to different departments whenever a student gets enrolled in a new course:
public static class StudentService
{
public static Student GetStudent(IEnumerable<Student> students, Func<Student, bool> predicate)
{
foreach (var student in students)
{
if (predicate(student))
{
return student;
}
}
return default;
}
public static void EnrollStudentToCourse(Student student, Course course, Action<Student> notify)
{
student.Courses.Add(course);
notify(student);
}
}
The EnrollStudentToCourse()
method adds a new course to the student's courses list, and then calls the notify action.
Next, we update the Program
class to include the NotifyAdmin()
and NotifyAccount()
methods:
public static void NotifyAdmin(Student student)
{
Console.WriteLine($"{student.Name} is now enrolled in {student.Courses.Last().Name}.");
}
public static void NotifyAccount(Student student)
{
var course = student.Courses.Last();
var amount = course.Price;
Console.WriteLine($"Please deduct {amount}$ from {student.Name}'s account.");
}
Both NotifyAdmin()
and NotifyAccount()
matches the Action<Student>
that the EnrollStudentToCourse()
of the class StudentService
accepts as one of the arguments.
All is set, we can extend the Main
to enroll the student in a new course and see if the process involves the notification to the Admin and Account departments:
static void Main(string[] args)
{
// fetch students and courses
var students = Student.DummyStudents();
var courses = Course.DummyeCourses();
// get student with a name: Rahul
var student_Rahul = StudentService.GetStudent(students, (s) =>
// create an Action<Student> variable and assign a method
Action<Student> sendNotifications = NotifyAdmin;
// add another method to the sendNotifications
sendNotifications += NotifyAccount;
// call EnrollStudentToCourse passing student, course and send
StudentService.EnrollStudentToCourse(student_Rahul, courses.Fi
console.ReadLine();
}
We define a sendNotifications
variable of type Action<Student>
in Main
, assigning NotifyAdmin
to it.
We also add NotifyAccount
using sendNotification += NotifyAccount
.
The call to EnrollStudentToCourse()
invokes all the subscribers of the sendNotifications
action.
Indeed, from the result, we see that both NotifyAdmin()
and NotifyAccount()
gets called when a student gets enrolled in a course.
Rahul is now enrolled in ASP.NET CORE API course
Please deduct amount: 100 dollars from Rahul
Undeniably, we have just scratched the surface a bit, there is more to Action and Func to know about. For now, let's conclude our discussion.
Conclusion
Action and Func are the inbuilt delegates. We tried to answer the question - why they are important and how we can provide a layer of abstraction using delegates.
In this article, we implemented the callback functionality using Action & Func. We also achieved a simple pub/sub model using Action. Hopefully, this article clears the air on the usability of Action and Func.
Posted on January 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.