c# delegate vs action vs func: Which to Use?
c# delegate vs action vs func: Understand the differences between custom delegates, Action, and Func in C# and learn when each fits your code.
c# delegate vs action vs func requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to pass a method as an argument, store a callback, or implement an event-like pattern, C# gives you three closely related tools: custom delegates, Action, and Func. The choice between them is not about capability—any of them can invoke a method—but about clarity, intent, and how much ceremony you want in your code. This article breaks down the syntax, the behavioral differences, and the practical criteria for choosing one over the other.
What a Delegate Actually Is
A delegate is a type that represents a method signature. It defines the parameter list and return type of a method that can be assigned to it. The simplest declaration looks like this:
public delegate int MathOperation(int a, int b);
This delegate can hold any method that takes two integers and returns an integer. You can assign a named method, a lambda, or an anonymous method to it:
MathOperation add = (a, b) => a + b; MathOperation multiply = (a, b) => a * b; Console.WriteLine(add(3, 4)); // 7 Console.WriteLine(multiply(3, 4)); // 12
A delegate is more than a function pointer. It is a type that supports multicast, meaning you can combine multiple methods into one invocation list. That behavior is what makes delegates the foundation of events in C#.
Action and Func as Predefined Delegates
Action and Func are generic delegate types defined in the System namespace. They exist so you don't have to declare a custom delegate for every method signature you want to pass around. The difference between them is simple: Action represents a method that returns void, while Func represents a method that returns a value.
Action has overloads from zero to sixteen parameters:
Action log = () => Console.WriteLine("Logging"); Action<string> print = message => Console.WriteLine(message); Action<int, int> addAndPrint = (a, b) => Console.WriteLine(a + b);
Func also has overloads, but the last generic type parameter is always the return type:
Func<int> getNumber = () => 42; Func<int, int, int> add = (a, b) => a + b; Func<string, int> parse = s => int.Parse(s);
Notice that Func<int, int, int> means two parameters and one return value. The return type always appears last.
Comparing Declaration and Usage
To see the practical difference, consider a method that accepts a transformation function. With a custom delegate, you need to declare the delegate type first:
public delegate string StringTransformer(string input); public static string ApplyTransform(string input, StringTransformer transformer) { return transformer(input); }
With Func, the same method becomes more concise and does not require a separate declaration:
public static string ApplyTransform(string input, Func<string, string> transformer) { return transformer(input); }
The caller can pass a lambda directly in both cases, but the Func version reduces the number of type declarations in your codebase. For a one-off callback, Func or Action is often the better fit because it does not pollute the namespace with a dedicated type.
When to Use a Custom Delegate Instead
Custom delegates still have a place. They give the delegate a meaningful name, which can improve readability when the same signature is used across many places. For example, a domain-specific delegate like OrderProcessor communicates intent better than Func<Order, Order> in a large codebase.
Custom delegates also support ref and out parameters, which Action and Func do not. If you need to pass a parameter by reference, you must declare a custom delegate:
public delegate void TryParseHandler(string input, out int result); TryParseHandler handler = (input, out int result) => result = int.TryParse(input, out result) ? result : 0;
This is a rare requirement, but when it appears, Action and Func simply cannot express it.
Choosing Between Action and Func
The choice between Action and Func is almost always dictated by whether the method returns a value. If the callback should return something, use Func; if it should only perform a side effect, use Action. This rule keeps the code honest about its intent. Forcing a method that returns a value into an Action by discarding the result is possible but hides information and can mislead readers.
Consider a method that iterates over a collection and invokes a callback for each element. If the callback needs to return a value to control the loop, Func is the right choice:
public static void ProcessUntil<T>(IEnumerable<T> items, Func<T, bool> predicate) { foreach (var item in items) { if (!predicate(item)) break; } }
If the callback only needs to observe each element, Action is simpler:
public static void ForEach<T>(IEnumerable<T> items, Action<T> action) { foreach (var item in items) { action(item); } }
The signature of the method you are writing will usually make the decision obvious.
Performance and Allocation Considerations
Delegates in C# are reference types. Creating a delegate from a lambda that captures local variables may allocate a closure object on the heap. This is generally not a problem in typical application code, but in hot paths—like a loop that runs millions of times—the repeated allocation can add pressure on the garbage collector.
A common pattern to reduce allocation is to cache the delegate instance when the lambda does not capture any variables. For example:
Func<int, int> square = static x => x * x; // static lambda, no capture for (int i = 0; i < 1000000; i++) { Use(square); }
Using the static keyword on a lambda prevents it from capturing variables from the enclosing scope, which avoids the closure allocation. If the lambda does not need to capture state, this can improve performance in tight loops. The same principle applies to Action and custom delegates.
Another subtle point is that multicast delegates—created by combining multiple delegates with + or +=—produce a new delegate instance each time. If you are adding handlers dynamically, be aware that the invocation list is immutable once created.
Common Mistakes and Edge Cases
One frequent mistake is assuming that Action and Func are interchangeable with custom delegates when it comes to covariance and contravariance. C# supports variance in delegate types, but only for reference types. For example, a Func<object> can be assigned to a Func<string> because string derives from object? Actually, it is the opposite: a Func<string> can be assigned to a Func<object> due to covariance. The rules are subtle and worth verifying for your specific scenario.
Another edge case is the null check. Invoking a delegate that is null throws a NullReferenceException. Always check for null before invoking, especially when the delegate is passed as a parameter:
public void Execute(Action action) { if (action == null) return; action(); }
Alternatively, use the null-conditional operator:
action?.Invoke();
This is a simple but common source of bugs.
Decision Guidance by Scenario
The following table summarizes the key criteria for choosing among custom delegates, Action, and Func:
| Scenario | Custom Delegate | Action | Func |
|---|---|---|---|
| Return type | Any, including void | void only | Any non-void |
| Named type | Yes, explicit declaration | No, generic | No, generic |
| ref/out parameters | Supported | Not supported | Not supported |
| Readability for one-off callbacks | Lower (extra declaration) | Higher | Higher |
| Readability for repeated domain logic | Higher | Lower | Lower |
| Multicast support | Yes | Yes | Yes |
| Lambda capture allocation | Same as Action/Func | Same as custom | Same as custom |
Use a custom delegate when the same signature appears frequently in your domain and a descriptive name adds meaning. Use Action when the callback returns no value and the signature is short. Use Func when the callback must return a value and the signature is short. For callbacks that are used only once, prefer Action or Func to avoid unnecessary type declarations.
Where This Choice Breaks Down in Practice
One practical limitation of Action and Func is that they cannot have optional parameters or parameter names that appear in the delegate type. When you declare a custom delegate, you can give the parameters meaningful names that appear in IntelliSense and documentation. With Action and Func, the parameter names are generic (arg1, arg2) unless you use a lambda with named parameters, which only helps at the call site.
If you are building a public API where the delegate signature is part of the contract, a custom delegate with well-named parameters can be more self-documenting. For internal code, the brevity of Action and Func usually wins.
Another edge case is when you need to pass a delegate as a generic type argument. Action and Func work seamlessly because they are generic types themselves. A custom delegate requires a concrete type, which can make generic algorithms more cumbersome. For example, a method that accepts Func<T, TResult> is flexible; a method that accepts a custom delegate is not generic without additional constraints.
Final Code Example: Combining Both Approaches
A realistic pattern is to use Func for the core transformation and a custom delegate for a domain-specific operation that needs a name. Consider a validation pipeline:
public delegate bool Validator(string value); public static bool Validate(string input, Validator validator) { return validator(input); } public static bool ValidateWithFunc(string input, Func<string, bool> validator) { return validator(input); }
Both methods work identically. The custom delegate version makes the intent explicit in the method signature, while the Func version is more concise. The choice depends on whether the delegate type is part of a larger domain model or just a local utility.
In most code you write, Action and Func will be sufficient. Custom delegates become valuable when you need to express a specific contract that appears repeatedly, or when you need ref or out parameters. Knowing the difference lets you pick the tool that keeps your code readable and maintainable without adding unnecessary abstraction.