Delegates let us define and work with methods as objects.
Two commonly used delegate types are Action and Func. They are first-class entities.
These entities can be treated and manipulated in the same way as other data types or objects.
Functions and methods are commonly static, but we can use them as a standard object variable within their data type signature.
We will explore using Action and Func delegates using the provided C# code, found in my GitHub, as a foundation.
Let’s start with the beginning
What is Delegates?
Delegates represent references to methods with specific parameter lists and return types. They are similar to function pointers in languages like C or C++, enabling us to pass methods like parameters to other processes, store them in variables, and invoke them dynamically at runtime.
Delegates like Action and Func and lambda expressions are examples of first-class entities, allowing for dynamic and flexible programming paradigms, such as functional programming, event handling, and callback mechanisms. This feature enhances code modularity, reusability, and expressive programming capabilities.
Action Delegate
There is a pre-defined delegate type called the Action, which works as a variable. This Action delegate represents a method that does not return any value.
Let’s see how it works: An action delegate can represent different methods in different moments:
Action displayMessageAction = DisplayMessage1;
displayMessageAction(); // This will call DisplayMessage1
displayMessageAction = DisplayMessage2;
displayMessageAction(); // This will call DisplayMessage2
The Action displayMessageAction was changed to DisplayMessage2 in runtime, and the console output will be:
This is a call from an Action delegate 1!
This is a call from an Action delegate 2!
Using Action, we can have a single variable in runtime call different methods. We can change the method it points to according to the environment, data type, or any other conditions.
Let’s consider another Action delegate in this Func delegate called WhatsApp:
Action displayMessageAction = DisplayMessage1;
displayMessageAction();
displayMessageAction = DisplayMessage2;
displayMessageAction();
static void DisplayMessage1()
{
Console.WriteLine("This is a call from an Action delegate 1!");
}
static void DisplayMessage2()
{
Console.WriteLine("This is a call from an Action delegate 2!");
}
public Func<Messages, Action> WhatsApp { get; } = messages => () =>
{
…
};
This is the implementation of a delegate Func, WhatsApp, that returns an Action. Let’s see in our example below how it works:
The source above does not show a notificationService and a record variable, but observing the sender is essential.Invoke(), which will call the implementation according to the record.TypeMessage without explicitly knowing it.
Here is the implementation of WhatsApp:
public Func<Messages, Action> WhatsApp { get; } = messages => () =>
{
if (messages.TypeMessage != 0) throw new Exception("Type mismatch");
Console.WriteLine($"Sending WhatsApp message to {messages.Recipient}: {messages.Message}");
};
We can read that WhatsApp returns an Action () =>. When called, it takes a Messages object as input. Suppose the message type is not TypeMessages.WhatsApp throws an exception for type mismatch. Otherwise, it sends, in theory, a message to the specified recipient.
As demonstrated, the Action delegate is frequently used for procedures that carry out activities or have associated results.
Func Delegate
Action sender;
switch (record.TypeMessage)
{
case (int)TypeMessageEnum.WhatsApp:
sender = notificationService.WhatsApp(record);
break;
case (int)TypeMessageEnum.Sms:
sender = notificationService.Sms(record);
break;
case (int)TypeMessageEnum.Email:
sender = notificationService.Email(record);
break;
default:
Console.WriteLine($"Type mismatch {record.TypeMessage}");
continue;
}
sender.Invoke();
The Func delegate is another pre-defined delegate type representing a method with parameters and a return value. It can have up to 16 parameters, with the last one specifying the return type. In our code, we’ve utilized Func in the following way:
public Func<Messages, Action> WhatsApp { get; } = messages => () =>
{
// ...
};
The Func delegate takes a Messages parameter and returns an Action. In this sample, we can use it to create functions that return actions for different message types (WhatsApp, SMS, Email).
The last param in Func is his return type. The example above is an Action. Let’s see a practical example:
Func<int, int, long> add = (x, y) => x + y;
var result = add(5, 10);
Console.WriteLine("Result: " + result);
As the last type in Func is long, the result of add(5, 10) is a long .NET type.
What is the difference between Action and Func delegates?
The primary distinction between Action and Func delegates is based on their return types and intended purpose. Action delegates are intended for methods that do not return a value and conduct actions or have side effects. Func delegates, on the other hand, are designed for processes that return a value and have a return type given as their last parameter.
Because of this fundamental distinction, Action may represent methods that execute tasks without delivering data, whereas Func can represent methods that compute and produce a result. The decision between Action and Func is determined by whether we need to conduct an action or obtain a value from the method encapsulated by the delegate.
Practical example
This code will read the CSV file, parse each row into a Messages object, and then use the delegate functions in SendNotifications to send the messages based on their TypeMessage property. If the TypeMessage value doesn’t match any expected types (0 for WhatsApp, 1 for SMS, 2 for Email), it will print an error message.
Here we read from the console the file path:
Console.WriteLine("Please type the CSV file path:");
var file = Console.ReadLine();
We create the notificationServices and call ProcessFile from the SenderProcess class:
var notificationService = new SendNotifications();
var procSenders = new SenderProcess();
try
{
var result = procSenders.ProcessFile(file, notificationService);
if (result == null)
{
Console.WriteLine("Result is null!");
}
else
{
var totalProcessed = result.CountTypes.Sum();
Console.WriteLine($"Total WhatsApp processed: {notificationService.CountTypes[0]}");
Console.WriteLine($"Total Sms processed: {notificationService.CountTypes[1]}");
Console.WriteLine($"Total Email processed: {notificationService.CountTypes[2]}");
Console.WriteLine($"Total messages processed: {totalProcessed}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error processing: {ex.Message}");
}
The ProcessFile transforms the file into a List Messages in the local function Read() and, after that, calls ProcessListMessages:
var processMessages = new SenderProcess();
// ...
processMessages.ProcessListMessages(records, notificationService, Processed);
Let's look again into how we've applied Action and Func delegates to send messages based on their types in our code.
public void ProcessRecords(
List<Messages>? records,
SendNotifications notificationService,
Action<int, SendNotifications> processed)
{
try
{
foreach (var record in records)
{
Action sender;
switch (record.TypeMessage)
{
case 0:
sender = () => notificationService.WhatsApp(record);
break;
case 1:
sender = () => notificationService.Sms(record);
break;
case 2:
sender = () => notificationService.Email(record);
break;
default:
Console.WriteLine($"Type mismatch {record.TypeMessage}");
continue;
}
sender.Invoke();
processed(record.TypeMessage, notificationService);
}
}
catch
{
throw new Exception("Error on records.");
}
}
In this method, we receive a collection of Messages records and use the Action delegate to send messages based on their TypeMessage. The Func delegates within notificationService return the appropriate actions for each message type, and the notificationService methods are all Func type.
How do delegates enhance code modularity and reusability in C# applications?
Delegates improve C# code modularity by encapsulating certain behaviors or operations, facilitating clear separation and easy feature management. They promote reusability by allowing delegate instances to be reused, allowing the same behavior to be used across several areas of an application.
Furthermore, delegates support expressive programming through functional paradigms, event handling, and callback mechanisms. This results in more concise, readable, and adaptable code that describes program behavior at a higher level, ultimately contributing to modular, reusable, and expressive C# applications.
Conclusion
Action and Func delegates are handy tools for managing methods as first-class entities. They allow us to encapsulate specific actions and functions, which results in code that is clearer and easier to maintain in our C# applications. When we design code that is both flexible and modular, it is vital to have a solid understanding of how to use these delegates.
Accecompletehe full example at my GitHub https://github.com/jssmotta/DelegateActionAndFuncAppSample