C# Pass in a Func to override behavior
By FoxLearn 1/21/2025 4:08:29 AM 57
In C#, function pointers are called delegates, and the most common ones are Action and Func. The key difference is that Func returns a value, while Action does not.
For example:
/// <summary> /// Default formatter = binary. Pass in a formatter function to change this behavior. /// </summary> static void PrintNumbers(int[] data, Func<int, string> formatterFunc = null) { if (formatterFunc == null) { formatterFunc = (n) => Convert.ToString(n, 2); // Default to binary formatting } for (int i = 0; i < data.Length; i++) { Console.WriteLine($"Number {i} = {formatterFunc(data[i])}"); } } static void Main(string[] args) { int[] numbers = new int[] { 8, 15, 23, 42 }; // Default behavior: binary format PrintNumbers(numbers); // Override: hexadecimal format PrintNumbers(numbers, (n) => n.ToString("X")); // Override: string representation PrintNumbers(numbers, (n) => $"Number: {n}"); }
Output:
- Binary format:
1000
,1111
,10111
,101010
- Hexadecimal format:
8
,F
,17
,2A
- Custom format:
Number: 8
,Number: 15
,Number: 23
,Number: 42
What is Func?
In the example, I use Func<int, string>
, where Func
is a type that specifies the method signature. This means I can pass in any method that matches the signature (int) => string
.
For instance, methods like these are valid with Func<int, string>
:
string Method1(int n)
string Method2(int n)
Func
can handle more parameters too, and you can have multiple Func
types for different use cases.
Examples of Func
:
Func Type | Example Method |
---|---|
Func<int> | int GetNumber() |
Func<int, int> | int Add(int a, int b) |
Func<string, int> | int ParseNumber(string s) |
Why Not Use an Interface or Class Instead?
You could implement this behavior with an interface or a class, which is another way to apply the Strategy Pattern. However, using interfaces or classes may introduce unnecessary verbosity for simple cases like this.
public interface INumberFormatter { string Format(int n); } public class DefaultFormatter : INumberFormatter { public string Format(int n) => Convert.ToString(n, 2); // Default to binary } public class HexFormatter : INumberFormatter { public string Format(int n) => n.ToString("X"); } static void PrintNumbers(int[] data, INumberFormatter formatter = null) { if (formatter == null) { formatter = new DefaultFormatter(); // Default formatter } for (int i = 0; i < data.Length; i++) { Console.WriteLine($"Number {i} = {formatter.Format(data[i])}"); } } static void Main(string[] args) { int[] numbers = new int[] { 8, 15, 23, 42 }; PrintNumbers(numbers); PrintNumbers(numbers, new HexFormatter()); }
While this works, passing an interface to implement the Strategy Pattern in this example is a bit overcomplicated compared to just passing a delegate.
Why Not Just Use a Flag?
You might wonder, why not just pass in a flag to determine how the method behaves?
public enum NumberFormats { Binary, Hex, Custom } static void PrintNumbers(int[] data, NumberFormats format = NumberFormats.Binary) { for (int i = 0; i < data.Length; i++) { string formatted = ""; int n = data[i]; switch (format) { case NumberFormats.Binary: formatted = Convert.ToString(n, 2); break; case NumberFormats.Hex: formatted = n.ToString("X"); break; case NumberFormats.Custom: formatted = $"Number: {n}"; break; } Console.WriteLine($"Number {i} = {formatted}"); } } static void Main(string[] args) { int[] numbers = new int[] { 8, 15, 23, 42 }; PrintNumbers(numbers); PrintNumbers(numbers, NumberFormats.Hex); PrintNumbers(numbers, NumberFormats.Custom); }
In this version, you would need to modify the PrintNumbers()
method each time you add a new format. This violates the Open-Closed Principle (code should be open to extension but closed to modification). Moreover, the PrintNumbers()
method becomes bloated, violating the Single Responsibility Principle.
Using a delegate (or function pointer) simplifies extending functionality. To add a new formatting behavior, you would simply pass in a new function, leaving the method unmodified.
- Using the OrderBy and OrderByDescending in LINQ
- Querying with LINQ
- Optimizing Performance with Compiled Queries in LINQ
- MinBy() and MaxBy() Extension Methods in .NET
- SortBy, FilterBy, and CombineBy in NET 9
- Exploring Hybrid Caching in .NET 9.0
- Using Entity Framework with IDbContext in .NET 9.0
- Primitive types in C#