While passing objects as arguments is a standard and familiar way to invoke methods, providing methods as arguments to other methods is less so. Nonetheless, we often must pass a method as a parameter to another method when working with event handling in C#. We do this using delegates.
I provided an overview of delegates in an earlier article here. In this article, we’ll examine how we can work with Action, Func, and Predicate delegates in C#. To work with the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.
A delegate is a type-safe function pointer that can reference a method that has the same signature as that of the delegate. Delegates are used to define callback methods and implement event handling, and they are declared using the “delegate” keyword. You can declare a delegate that can appear on its own or even nested inside a class.
What are Func and Action delegates? How can they be used?
The most common delegates in C# are the Func delegate and the Action delegate. Both are reference types that encapsulate a method. The Func delegate points to a method that accepts parameters and returns a value; the Action delegate points to a method that accepts parameters but does not return a value (i.e., returns void).
Both of these delegate objects will take zero or many parameters, and we can use them with lambda expressions or anonymous methods.
The syntax for creating an action delegate in C# is
Action<TParameter>
We can create an Action delegate in C# using the Action keyword.
Action<string> actionDelegate = new Action<string>(DisplayText);
actionDelegate("Hello World!");
The syntax for declaring a Func delegate in C# is
Func<TParameter, TOutput>
To create a function delegate in C#, we use the Func keyword.
public class DelegateHelper
{
Func<double, double> functionDelegate = new Func<double, double>(GetTax);
public static double GetTax(double netSalary)
{
return (double)(netSalary * .3);
}
}
Console.WriteLine(DelegateHelper.GetTax(100000));
Here the DelegateHelper class contains a static method named GetTax, which calculates and returns tax based on net salary, and a delegate that points to this method. A Func delegate has been used to invoke the GetTax method.
Using Action delegates in C#
The following code listing provides another example of how you can use an Action delegate. This code snippet when executed would print the word “Hello!!!” in the console window.
static void Main(string[] args)
{
Action<string> action = new Action<string>(Display);
action("Hello!!!");
Console.Read();
}
static void Display(string message)
{
Console.WriteLine(message);
}
Using Func delegates in C#
And here is a second example of using a Func delegate in C#. The following code snippet prints the value of a health reimbursement account, or HRA (calculated as 40% of basic salary). The base salary is passed to the delegate as an argument.
static void Main(string[] args)
{
Func<int, double> func = new Func<int, double>(CalculateHra);
Console.WriteLine(func(50000));
Console.Read();
}
static double CalculateHra(int basic)
{
return (double)(basic * .4);
}
Note that the second parameter in the declaration of the Func delegate in the code snippet given earlier represents the return type of the method to which the delegate would point. In this example, the calculated Hra value is returned as double.
Using Predicate delegates in C#
A Predicate is a delegate that accepts one or more generic parameters and returns a boolean value. Predicate delegates are typically used to perform search operations based on a set of criteria.
Here is the syntax for a Predicate delegate.
Predicate<T>
Note that Predicate<T> is basically equivalent to Func<T,bool>.
Consider the following entity class named Customer.
class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
}
Next, create a list of customers and store objects of type Customer into it.
List<Customer> custList = new List<Customer>();
custList.Add(new Customer { Id = 1, FirstName = "Joydip", LastName = "Kanjilal", State = "Telengana", City = "Hyderabad", Address = "Begumpet", Country = "India" });
custList.Add(new Customer { Id = 2, FirstName = "Steve", LastName = "Jones", State = "OA", City = "New York", Address = "Lake Avenue", Country = "US" });
The following is the complete code listing that shows how we can use a Predicate delegate to search data.
static void Main(string[] args)
{
List<Customer> custList = new List<Customer>();
custList.Add(new Customer { Id = 1, FirstName = "Joydip", LastName = "Kanjilal", State = "Telengana", City = "Hyderabad", Address = "Begumpet", Country = "India" });
custList.Add(new Customer { Id = 2, FirstName = "Steve", LastName = "Jones", State = "OA", City = "New York", Address = "Lake Avenue", Country = "US" });
Predicate<Customer> hydCustomers = x => x.Id == 1;
Customer customer = custList.Find(hydCustomers);
Console.WriteLine(customer.FirstName);
Console.Read();
}
When the above code snippet is executed, the name “Joydip” will be displayed in the console window.
Using variance in delegates in C#
When you define your delegates, you can use covariance and contravariance to give your delegate a greater level of flexibility. The covariance approach makes it possible for a method to return a more derived type than that defined in the delegate. Using contravariance, you can define a method that accepts less derived arguments than those in the delegate type.
The following code snippet illustrates covariance in delegates.
class TypeA {}
class TypeB : TypeA {}
public delegate TypeA MyDelegate();
public static TypeA HandlerA()
{
//Some code
}
public static TypeB HandlerB()
{
//Some code
}
MyDelegate delegateA = HandlerA;
MyDelegate delegateB = HandlerB;
Now, consider the following two delegate declarations.
public delegate void KeyEventHandler(object sender, KeyEventArgs e)
public delegate void MouseEventHandler(object sender, MouseEventArgs e)
Because EventArgs is the base class of both KeyEventArgs and MouseEventArgs, you can take advantage of contravariance to combine them into just one handler that is capable of handling both key and mouse events as shown in the code snippet given below.
private void MyCommonHandler(object sender, System.EventArgs e)
{
//Some code
}
Delegates are among the most widely used features of the C# programming language. They are ideally suited to implementing event-driven programming. In most cases, the return type of the method pointed to by a delegate must be identical to the type specified in the delegate’s declaration. However, we can turn to covariance and contravariance to gain more flexibility.