C# Action vs Func: Choosing the Right Delegate
c# action vs func: Understand the difference between Action and Func delegates in C#, including return types, type parameter order, async variants, and practical selec...
c# action vs func requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The core difference between Action and Func in C# is the return type. Action represents a method that returns void, while Func represents a method that returns a value. That single distinction drives every practical decision between the two, and it is the first thing to check when a delegate does not compile at the call site.
Action<string> log = message => Console.WriteLine(message); Func<int, int> square = x => x * x;
The Action<string> delegate accepts a string parameter and returns nothing. The Func<int, int> delegate accepts an int and returns an int. In every Func declaration, the last type parameter is the return type; all preceding type parameters are input types.
Declaration and Type Parameter Order
Func places the return type last in its type parameter list. Action has no return type parameter at all because the return type is always void.
| Delegate type | Type parameters | Meaning |
|---|---|---|
Action | none | No input parameters, returns void |
Action<T> | one | One input parameter, returns void |
Func<TResult> | one | No input parameters, returns TResult |
Func<T, TResult> | two | One input parameter, returns TResult |
Both types provide overloads up to sixteen input parameters. The framework defines Action with zero through sixteen parameters, and Func with one through sixteen inputs plus a return type. If you need more than sixteen parameters, you must declare a custom delegate type.
Using Action for Side Effects
Action is the right choice when the delegate's job is to perform an operation rather than produce a result. Logging, event notification, and mutating an object are typical examples.
void ProcessItems(IEnumerable<Item> items, Action<Item> processor) { foreach (var item in items) { processor(item); } } ProcessItems(items, item => item.Status = Status.Processed);
The method ProcessItems iterates over a collection and invokes the Action<Item> for each element. The caller decides what the side effect is. This keeps the iteration logic in one place while allowing different callers to apply different mutations without duplicating the loop.
Using Func for Transformations and Queries
Func is the natural fit when the delegate must compute a value. LINQ's Select, Where, and Aggregate methods all rely on Func because they need to transform or filter data and return results.
Func<Order, decimal> calculateTotal = order => order.LineItems.Sum(line => line.Price * line.Quantity); var totals = orders.Select(calculateTotal);
The Func<Order, decimal> delegate takes an Order and returns a decimal. Passing it to Select produces a sequence of order totals. If you tried to use an Action here, the code would not compile because Select expects a function that returns a value for each input element.
Method Groups and Lambda Conversion
Both Action and Func can be assigned from method groups, not just lambdas. This matters when you already have a method whose signature matches the delegate type.
void SaveToDatabase(Customer customer) { /* ... */ } decimal GetDiscount(Customer customer) { return 0.1m; } Action<Customer> save = SaveToDatabase; Func<Customer, decimal> discount = GetDiscount;
The compiler performs overload resolution against the delegate signature. If the method returns a value, it can only be assigned to a Func or a custom delegate with a return type. If it returns void, it can only be assigned to an Action. Assigning a value-returning method to an Action produces a compile-time error, which is the most common symptom developers encounter when first working with these types.
Async Variants
When a delegate represents an asynchronous operation, the return type becomes more significant. An async void method cannot be awaited by the caller, which makes Action a poor fit for operations where the caller needs to observe completion or handle exceptions.
Func<Task<int>> fetchCount = async () => { var data = await LoadDataAsync(); return data.Count; };
An async method that returns Task or Task<T> should be assigned to a Func returning Task or Task<T>, not to an Action. Action is acceptable for async void handlers in UI event scenarios where the event system manages the synchronization context, but in library code, Func<Task> is the safer choice because exceptions can be observed and awaited by the caller.
Performance and Allocation Overhead
The runtime cost difference between Action and Func is negligible in most cases. Both are delegate types with the same invocation mechanism. The main allocation concern comes from closures: when a lambda captures variables from the enclosing scope, the compiler generates a closure object that may be allocated on each invocation.
int threshold = 10; Func<int, bool> isAboveThreshold = value => value > threshold;
Here threshold is captured, so the delegate holds a reference to a closure. If this code runs inside a hot loop, the closure allocation repeats on every iteration. If the lambda captures nothing, the compiler can cache the delegate instance statically and avoid repeated allocation.
The choice between Action and Func does not change this behavior. What matters is whether the delegate captures state and how frequently it is created. A delegate that is created once and reused has minimal overhead regardless of which type you choose.
Choosing Between Action and Func
The decision rule is straightforward: if the code needs a result from the delegate, use Func. If it only needs an operation performed, use Action.
Consider a repository method that either saves an entity or returns a projection of it:
void Save<T>(T entity, Action<T> beforeSave) { beforeSave(entity); _context.Save(entity); } TResult Project<T, TResult>(T entity, Func<T, TResult> projector) { return projector(entity); }
Save accepts an Action<T> because the caller mutates the entity before persistence. Project accepts a Func<T, TResult> because the caller computes a derived value from the entity.
A common mistake is to use Action when a value is needed, or Func when the delegate never returns anything meaningful. The compiler catches the first error at the call site. The second mistake leads to confusing code where every lambda ends with return; or returns a dummy value just to satisfy the signature. If the delegate produces no useful result, Action is the clearer contract.
Edge Cases and Compatibility
When you need a delegate that returns a value but also has side effects, Func still works. The side effect happens when the delegate is invoked, and the return value is delivered to the caller. There is no rule that a Func must be pure.
One limitation to keep in mind: Action and Func are generic delegate types with predefined signatures. If you need ref or out parameters, these types do not support them. You must declare a custom delegate:
public delegate bool TryParseHandler<T>(string input, out T result);
Similarly, if you need a delegate with a variable number of parameters or a specific parameter modifier such as in, the predefined Action and Func types will not fit. In those cases, a custom delegate declaration is the only option, and it should be named according to its purpose rather than forced into the Action or Func naming convention.