C# Func, Action, and Predicate
Func, Action, and Predicate are built-in generic delegate types in C#. They let you store, pass, and call methods or lambda expressions without declaring a custom delegate every time. They matter because callbacks, LINQ queries, validation rules, logging hooks, and small pieces of reusable behavior become simple, type-safe values.
Overview: How Func, Action, and Predicate Work
A delegate is a type-safe reference to a method. Func, Action, and Predicate are ready-made delegate types in the .NET base class library. Instead of writing delegate int Operation(int x, int y);, you can often write Func<int, int, int>. Both describe something callable that accepts two integers and returns an integer.
Func represents a method that returns a value. Its final generic type argument is always the return type. For example, Func<string, int> means a callable value that takes a string and returns an int. Func<int, int, bool> takes two integers and returns a Boolean. There is also Func<TResult> for a function with no parameters.
Action represents a method that returns void. Use it for work that performs an effect: printing, logging, saving, notifying, or updating an object. Action<string> accepts one string and returns nothing. Plain Action accepts no parameters and returns nothing.
Predicate<T> represents a method that accepts one T and returns bool. It is specialized for yes-or-no questions such as IsInStock, IsValid, or IsExpensive. Predicate<Product> is equivalent in shape to Func<Product, bool>, but the name communicates that the delegate is a test.
Internally, these types are normal delegate classes derived from MulticastDelegate. A delegate instance stores the method to call and, for instance methods or capturing lambdas, the target object that supplies state. When a lambda captures a local variable, the compiler creates a hidden closure object so the delegate can use that variable later. This is powerful, but it also means closures can keep objects alive longer than expected.
These built-in delegates are heavily used by LINQ. Methods such as Where, Select, Any, and OrderBy receive functions describing what to test, transform, or compare. Even when you are not writing LINQ, the same pattern lets you separate an algorithm from the behavior it needs.
Syntax
Func<TInput, TResult> converter = value => result;
Func<T1, T2, TResult> combine = (first, second) => result;
Action<T> doSomething = value => { /* statements */ };
Action noArguments = () => { /* statements */ };
Predicate<T> test = value => true;
| Form | Meaning |
|---|---|
Func<TResult> |
No parameters, returns TResult. |
Func<T, TResult> |
One parameter of type T, returns TResult. |
Func<T1, T2, TResult> |
Two parameters, returns the final type argument. |
Action |
No parameters, returns void. |
Action<T> |
One parameter, returns void. |
Predicate<T> |
One parameter of type T, returns bool. |
The lambda operator => reads as “goes to.” Parameter types are usually inferred from the delegate type, so Func<int, int> square = x => x * x; gives x the type int.
Examples
Using Func to Choose a Calculation
using System;
class Program
{
static void Main()
{
Func<int, int, int> operation = Add;
Console.WriteLine(operation(6, 4));
operation = (left, right) => left * right;
Console.WriteLine(operation(6, 4));
}
static int Add(int left, int right)
{
return left + right;
}
}
Output:
10
24
Func<int, int, int> means two int inputs and one int result. The variable first stores a named method, then stores a lambda expression. The caller uses the same invocation syntax either way.
Using Action for Reusable Output Behavior
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> names = new List<string> { "Ada", "Grace", "Linus" };
PrintEach(names, name => Console.WriteLine($"Hello, {name}!"));
PrintEach(names, name => Console.WriteLine(name.ToUpperInvariant()));
}
static void PrintEach(List<string> items, Action<string> printer)
{
foreach (string item in items)
{
printer(item);
}
}
}
Output:
Hello, Ada!
Hello, Grace!
Hello, Linus!
ADA
GRACE
LINUS
PrintEach owns the loop, but the caller supplies what should happen for each item. This is a common callback pattern: one method controls the timing, while an Action controls the side effect.
Using Predicate to Filter Objects
using System;
using System.Collections.Generic;
public record Product(string Name, decimal Price, bool InStock);
class Program
{
static void Main()
{
List<Product> products = new List<Product>
{
new Product("Keyboard", 49.99m, true),
new Product("Monitor", 219.00m, false),
new Product("Mouse", 24.50m, true),
new Product("Dock", 129.00m, true)
};
List<Product> matches = Find(products, product => product.InStock && product.Price < 100m);
foreach (Product product in matches)
{
Console.WriteLine(product.Name);
}
}
static List<Product> Find(List<Product> products, Predicate<Product> match)
{
List<Product> results = new List<Product>();
foreach (Product product in products)
{
if (match(product))
{
results.Add(product);
}
}
return results;
}
}
Output:
Keyboard
Mouse
The predicate is the rule: return true for products that should be included. The filtering method does not know or care whether the rule checks stock, price, name, or a combination of fields.
Combining Func and Action in a Small Pipeline
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<decimal> prices = new List<decimal> { 12.5m, 40m, 100m };
ProcessPrices(
prices,
price => price * 1.08m,
total => Console.WriteLine($"Final: {total:0.00}"));
}
static void ProcessPrices(List<decimal> prices, Func<decimal, decimal> addTax, Action<decimal> report)
{
foreach (decimal price in prices)
{
decimal finalPrice = addTax(price);
report(finalPrice);
}
}
}
Output:
Final: 13.50
Final: 43.20
Final: 108.00
This example separates transformation from reporting. The Func calculates a new value, while the Action consumes that value. The same method could later use a different tax rule or a different reporting target.
How It Works Step by Step
- The compiler sees a variable or parameter such as
Func<decimal, decimal>and knows the exact call signature required. - When you assign a method group or lambda, the compiler checks that the parameter types and return type match the delegate.
- For a non-capturing lambda, the compiler can create a delegate to a generated static method or reuse a cached delegate in many cases.
- For a capturing lambda, the compiler creates a closure object containing the captured variables, then creates a delegate that points to a method on that closure object.
- At runtime, invoking the delegate calls its
Invokemethod, which dispatches to the stored method and target. - If multiple delegates are combined, their invocation list runs in order. This is more common with
ActionthanFunc, because only the last return value is returned.
The compiler does not treat Func, Action, or Predicate as dynamic calls. They are strongly typed delegate instances. A mismatched lambda fails at compile time, before the program can run.
Common Mistakes
Putting the Func Return Type in the Wrong Position
Func<int, string> parseAndAdd = (number, text) => number + int.Parse(text);
This does not compile because Func<int, string> describes one int parameter and a string return value. It does not describe two parameters. In Func, read the generic arguments as parameters first and result last.
using System;
class Program
{
static void Main()
{
Func<int, string, int> parseAndAdd = (number, text) => number + int.Parse(text);
Console.WriteLine(parseAndAdd(5, "7"));
}
}
Output:
12
Using Action When a Result Is Needed
Action<int> square = value => value * value;
This does not compile because Action<int> must return void. If the caller needs a computed value, use Func.
using System;
class Program
{
static void Main()
{
Func<int, int> square = value => value * value;
Console.WriteLine(square(9));
}
}
Output:
81
Capturing a Variable That Later Changes
using System;
class Program
{
static void Main()
{
int threshold = 10;
Predicate<int> isLarge = value => value > threshold;
threshold = 20;
Console.WriteLine(isLarge(15));
}
}
Output:
False
The predicate captures the variable threshold, not a frozen copy of the value 10. When threshold changes to 20, the predicate observes the new value. If you need a fixed value, copy it into a separate local that you do not change.
Best Practices
- Use
Funcwhen the delegate returns a value, and remember that the return type is the final generic argument. - Use
Actionfor callbacks that perform work and returnvoid, such as logging, printing, or notification. - Use
Predicate<T>when the delegate is clearly a one-parameter true-or-false test. - Prefer a custom delegate name for public APIs when the role deserves domain language, such as
PaymentValidatororRetryPolicy. - Keep delegate signatures small. If a callback needs many parameters, pass a small request object or record instead.
- Avoid hidden side effects in
Funccallbacks. Readers usually expect functions to compute and return values. - Be careful with closures over mutable variables, especially in loops, asynchronous callbacks, and long-lived delegates.
- Use existing LINQ methods before writing your own filtering or mapping loops when LINQ communicates the intent clearly.
Practice Exercises
- Create a
Func<string, int>that returns the length of a string. Use it to print the lengths of three names. - Write a method named
Repeatthat accepts anint countand anAction<int>. It should call the action with values from1throughcount. - Create a list of numbers and a
Predicate<int>namedisOdd. Print only the odd numbers from the list.
Summary
Func,Action, andPredicateare built-in generic delegate types.Funcreturns a value, with the return type written last.Actionreturnsvoidand is best for effects or notifications.Predicate<T>is a readable way to represent a one-parameter Boolean test.- Lambdas assigned to these delegates are checked at compile time for parameter and return compatibility.
- Captured variables remain live through closures, so mutable captured state should be used deliberately.
