Func, Action and Predicate

The built-in delegate types: Action returns nothing, the last Func type argument is its return type, and Predicate is a test.

SyntaxAction<T1, ... T16> | Func<T1, ... T16, TResult> | Predicate<T>

Declaring a delegate type for every callback gets repetitive. .NET ships generic delegate types that cover almost every shape, so most code never declares one.

Reading the type arguments

Action holds a method that returns nothing. Its type arguments are its parameters. Action<string> takes one string; Action<int, int> takes two int values; plain Action takes none.

Func holds a method that returns a value, and the last type argument is the return type. Func<int> takes nothing and returns an int. Func<string, bool> takes a string and returns a bool. Func<int, string, bool> takes an int and a string and returns a bool. Both families go up to sixteen parameters.

Predicate

Predicate<T> is declared as public delegate bool Predicate<in T>(T obj);. It is a test: one value in, true or false out. That is the same shape as Func<T, bool>, but they are two different types, and one does not assign to the other.

Which one a method wants depends on its age. List<T>.FindAll, List<T>.RemoveAll and Array.Find ask for Predicate<T>. LINQ methods such as Where and Count ask for Func<T, bool>. New code should prefer Func<T, bool>. To cross between them, wrap: Predicate<int> p = f.Invoke; builds a Predicate<int> around a Func<int, bool> called f.

Example

Example · csharp
Action<string> log = message => Console.WriteLine($"log: {message}");
Func<int, int, int> add = (left, right) => left + right;
Predicate<int> isEven = n => n % 2 == 0;

log("starting");
Console.WriteLine(add(2, 3));

List<int> numbers = [1, 2, 3, 4, 5, 6];

// List<T>.FindAll asks for a Predicate<T>.
List<int> evens = numbers.FindAll(isEven);
Console.WriteLine(string.Join(", ", evens));

// LINQ asks for Func<T, bool>. Same shape, different type, so the test is
// written against whichever type the method it is handed to expects.
Func<int, bool> isEvenFunc = n => n % 2 == 0;
Console.WriteLine(numbers.Count(isEvenFunc));

// Output:
// log: starting
// 5
// 2, 4, 6
// 3

When to use it

  • Accept a factory method so a value is computed only when it is really needed.
  • Filter a List<T> in place with RemoveAll.
  • Write a helper that takes a transformation and applies it to every item.
  • Hand a progress callback to a method that takes a while.

More examples

Func for work you may not need to do

Taking a Func instead of a value means the expensive part is only computed when it is actually needed. Reach for this whenever a parameter is costly to produce and often unused.

Example · csharp
static class Cache
{
    private static readonly Dictionary<string, int> _values = new();

    // The caller passes a method that produces the value. It runs only on a miss.
    public static int GetOrAdd(string key, Func<string, int> factory)
    {
        if (_values.TryGetValue(key, out int existing))
        {
            return existing;
        }

        int created = factory(key);
        _values[key] = created;
        return created;
    }
}

// Called as:
// Console.WriteLine(Cache.GetOrAdd("abc", k => k.Length));   // prints 3

Action as a callback, Predicate as a filter

One Predicate is reused for both finding and removing, and the Action lets the caller decide what reporting means. This is the shape of most collection clean-up code.

Example · csharp
List<string> jobs = ["build", "test", "lint", "deploy"];

Action<string> report = name => Console.WriteLine($"removing: {name}");
Predicate<string> isShort = name => name.Length == 4;

foreach (string name in jobs.FindAll(isShort))
{
    report(name);
}

// RemoveAll takes a Predicate<string> and returns how many it removed.
int removed = jobs.RemoveAll(isShort);
Console.WriteLine($"{removed} removed, {jobs.Count} left");

// Output:
// removing: test
// removing: lint
// 2 removed, 2 left

Several parameters, return type last

Reading a long Func is a matter of counting: everything but the last type argument is a parameter. Useful when you need to recognise a type like Func<HttpRequest, string, int, bool> in someone else's signature.

Example · csharp
// Three inputs (string, int, bool) and a string result.
Func<string, int, bool, string> format = (name, count, upper) =>
{
    string text = $"{name} x{count}";
    return upper ? text.ToUpperInvariant() : text;
};

Console.WriteLine(format("widget", 3, false));
Console.WriteLine(format("widget", 3, true));

// Output:
// widget x3
// WIDGET X3

Discussion

  • Be the first to comment on this lesson.