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.
Action<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
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
// 3When 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.
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 3Action 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.
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 leftSeveral 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.
// 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