Delegates vs. Func and Action in C#: Are Custom Delegates Still Relevant?
If you often use delegates when writing C# code, you may want to consider Func and Action types instead. Keep on reading to learn why custom delegates may no longer be the most relevant choice.
In C#, a delegate is a type that represents a reference to a method. It acts like a pointer or a placeholder to a method with a specific signature (return type and parameters). Delegates allow you to store and invoke methods dynamically at runtime, enabling methods to be passed as parameters, assigned to variables, or used as event handlers.
A common use case for delegates is when you want to perform different actions on a number, such as logging it to the console or applying some mathematical operation before displaying the result. The following example demonstrates how you can use delegates to dynamically switch between these actions, showcasing the flexibility and power of delegates in C#:
class Program
{
// Definition of delegate that receives int and returns void
delegate void PrintDelegate(int number);
// First method that matches delegate signature
// Simple method that writes out the received number
static void PrintNumber(int number)
{
Console.WriteLine($"Number: {number}");
}
// Second method that matches delegate signature
// Method that writes out the square value of received number
static void PrintSquare(int number)
{
Console.WriteLine($"Number: {number*number}");
}
public static void Main()
{
// Creating an instance of the delegate
// Assigning first method to it
PrintDelegate print = PrintNumber;
print(5);
// Reassigning the second method to delegate
print = PrintSquare;
print(5);
}
}
As you can see, we can reassign the methods to the same instance of a delegate. The important thing is that methods have a signature or declaration that matches the delegate's signature.
What came first in C# — Delegates or Func and Action?
As you may assume, the delegates came first and were introduced with the very first version, C# 1.0, which was released in 2002. They have been a core feature of the language since its inception, providing a way to encapsulate method references and enabling event-driven programming and callback methods right from the start.
Introduction to Func and Action
Func is a delegate type in C# that represents a method that takes one or more input parameters and returns a value. It can accept up to 16 parameters, with the last parameter being the return type.
Example:
// A Func that receives int and returns an int value
// Returns the square of the given int
// Lambda expressions are often used with Func and Action
Func<int,int> square = x => x*x;
int result = square(5); // result is 25
Action, on the other hand, is a delegate type that represents a method that can also accept up to 16 parameters, but it doesn’t return a value.
Example:
// An Action that receives string and performs console log of it
// Uses Lambda expression to write out given message to console
Action<string> printMessage = message => Console.WriteLine(message);
printMessage("Hello, world!"); // prints: Hello, world!
Lambda expressions with Func and Action can shorten the code and make it cleaner. Simple tasks like calculating squares out of a given number or printing a message on the console can be simplified by using mentioned lambda expressions with Func and Action.
If we were to use delegates instead, we would need to define our custom delegate and then assign it a method that does the calculation or printing.
Let’s take a look at how would it be if we were to transform the given Func example into a delegate:
public class Program
{
// Defining the custom delegate that receives int and returns int
delegate int SquareDelegate(int x);
public static void Main()
{
// Assigning a lambda expression to the delegate
SquareDelegate square = x => x * x;
// Invoking the delegate
int result = square(5); // result is 25
}
}
As you can see, using delegates over Func or Action requires an additional piece of code where we will need to define our custom delegate. This code is also shortened with Lambda expression while, usually, when delegates are used we define the methods with matching signatures, then we assign them to the delegate instance and invoke the delegate.
Pros of using Func and Action instead of Delegates
- Simplicity and Readability — Func and Action are built-in types that provide a straightforward way to define delegates without needing to create custom delegate types. This makes your code cleaner and easier to understand.
- Aligning with modern C# practices — Using Func and Action is generally preferred when possible, as they align with modern C# practices and you can work seamlessly with lambda expressions and LINQ, providing a consistent and modern approach to method handling.
- Standardization — Func and Action are part of the .NET Framework and are standardized types across different .NET languages and platforms. They provide a consistent way to define and use delegates without needing to create custom delegate types for many common scenarios.
Cons of using Func and Action instead of Delegates
There’s only one con that I came to find out when it comes to using Func and Action instead of Delegates in your C# code and that is specific behavior that Func and Action do not cover of supporting “out” and “ref” parameters.
If you need to use “out” or “ref” parameters, you need to use delegates instead of Func and Action.
Let’s see a simple example of using delegates with the “out” parameter:
// Defining a custom delegate with an 'out' parameter
delegate void CalculateDelegate(int input, out int result);
public class Program
{
// Method that matches the delegate signature
static void Calculate(int input, out int result)
{
result = input * 2; // Example calculation
}
public static void Main()
{
// Creating an instance of the delegate and assigning the method to it
CalculateDelegate calculate = Calculate;
// Using the delegate with an 'out' parameter
int result;
calculate(5, out result);
Console.WriteLine($"Result: {result}"); // Prints: Result: 10
}
}
One example with the “ref” parameter:
// Defining a custom delegate with a 'ref' parameter
delegate void ModifyDelegate(ref int value);
public class Program
{
// Method that matches the delegate signature
static void Modify(ref int value)
{
value += 10; // Example modification
}
public static void Main()
{
// Creating an instance of the delegate and assigning the method to it
ModifyDelegate modify = Modify;
// Using the delegate with a 'ref' parameter
int value = 5;
modify(ref value);
Console.WriteLine($"Value: {value}"); // Prints: Value: 15
}
}
When using Func or Action, ref parameters are indeed a limitation, but out parameters can often be replaced by returning multiple values using a Func with a tuple or a custom class.
An example of Func that returns a Tuple:
// Defining a Func that returns a Tuple with the result and additional value
Func<int, (int result, int additionalValue)> calculate = input =>
{
int result = input * 2; // Example calculation
int additionalValue = input + 10; // Example additional value
return (result, additionalValue);
};
Or we can even use a custom class as the return type of the Func:
// Defining a custom class to hold the result and additional value
public class CalculationResult
{
public int Result { get; set; }
public int AdditionalValue { get; set; }
}
// Defining a Func that returns an instance of the custom class
Func<int, CalculationResult> calculate = input =>
{
return new CalculationResult
{
Result = input * 2, // Example calculation
AdditionalValue = input + 10 // Example additional value
};
};
This approach helps maintain code simplicity and readability, leveraging the built-in capabilities of Func and Action while avoiding the complexity of custom delegates for most common scenarios.
If you’ve been using delegates but haven’t tried Func and Action, I hope this article shows you the benefits of these modern alternatives.
Thanks for reading and happy coding!