C# Pass in a Func to override behavior

By FoxLearn 1/21/2025 4:08:29 AM   57
In C#, if I want to change the behavior of a method from the outside, I can pass in a function pointer. This technique exists in most programming languages and can be used to implement patterns like the Strategy Pattern.

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 TypeExample 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.