C# Func Delegate: Syntax, Usage, and Practical Examples
Learn how to use the C# Func delegate to pass methods as parameters, write concise lambdas, and build flexible, reusable code.
The C# Func delegate is the built-in tool for representing a method that returns a value. It lets you pass behavior as an argument, store it in a variable, or return it from another method without declaring a new delegate type. A Func delegate is a generic type: the last type parameter is the return type, and the preceding type parameters are the input types. The simplest form, Func<TResult>, takes no arguments and returns a value of type TResult.
Func<int> getNumber = () => 42; Console.WriteLine(getNumber()); // 42
When you need to accept input, add type parameters from left to right. For a method that takes an int and returns a string, the delegate signature is Func<int, string>.
Func<int, string> format = number => $"Value: {number}"; Console.WriteLine(format(5)); // Value: 5
The target delegate type provides the lambda's parameter types, and the lambda body determines the return type, so you rarely need to write those types explicitly. This makes Func delegates practical for LINQ queries, callbacks, and other behavior-based designs.
Func Delegate Syntax and Type Parameters
The System namespace defines multiple Func overloads, from Func<TResult> up to Func<T1, T2, ..., T16, TResult>. The last type argument is always the return type, and all preceding arguments are input types. This ordering is the same for every overload.
Func<int, int, int> add = (a, b) => a + b; Console.WriteLine(add(2, 3)); // 5
Because Func returns a value, it is useful for transformations, computations, and logic that needs to produce a result. Action delegates, on the other hand, return void and are used for operations that don't produce a value. The distinction matters when you choose which delegate type fits your method signature.
| Delegate Type | Returns | Typical Use |
|---|---|---|
Func<TResult> | value | computation, conversion, lookup |
Action | void | operation, side effect, mutation |
If a method does not return a value, use Action instead of Func. Trying to use Func with a void method is a compile error because the delegate's return type must match.
Passing a Method Group to a Func Delegate
A Func delegate can wrap an existing method through a method group conversion. This is useful when the logic already exists as a named method and you want to pass it as a parameter.
static string ConvertToUpper(string input) => input.ToUpperInvariant(); // Method group conversion Func<string, string> transform = ConvertToUpper; Console.WriteLine(transform("hello")); // HELLO
The compiler checks that the method's signature matches the delegate's signature. If ConvertToUpper takes a string and returns a string, it fits Func<string, string>. This approach is clearer than writing a lambda that simply calls the method, and it keeps the code DRY when the same method is used in multiple places.
Using Func as a Method Parameter
A common reason to use Func is to accept a transformation or condition as a parameter. This lets the caller supply custom behavior without altering the method's implementation.
public static List<int> TransformList(List<int> source, Func<int, int> transform) { var result = new List<int>(source.Count); foreach (var item in source) { result.Add(transform(item)); } return result; }
Callers can then pass a lambda, a method group, or an inline expression:
var numbers = new List<int> { 1, 2, 3 }; var doubled = TransformList(numbers, x => x * 2); var incremented = TransformList(numbers, x => x + 1);
This pattern centralizes the iteration logic while leaving the operation open to extension. It is the same principle behind LINQ's Select and Where methods, which accept Func<TSource, TResult> and Func<TSource, bool> respectively.
Capturing Variables in Lambdas
Lambda expressions assigned to Func delegates capture variables from the enclosing scope. The captured variables are stored with the delegate instance, and their values are read at invocation time, not at creation time. This behavior is useful but can lead to subtle bugs if the variable changes before the delegate runs.
int addend = 10; Func<int, int> addWith = x => x + addend; addend = 20; Console.WriteLine(addWith(5)); // 25, not 15
Be aware of captures when using Func in loops. Before C# 5, a foreach loop variable captured in a lambda was shared across iterations, causing all delegates to see the final value. C# 5 and later fixed this for foreach loops by creating a fresh capture per iteration, but you can still run into similar issues with for loops. If you need to capture an immutable snapshot, copy the value into a local variable inside the loop.
Func and Higher-Order Functions
A higher-order function is a function that takes another function as an argument or returns one as a result. Func enables both patterns. Returning a Func from a method allows you to create specialized functions on the fly.
static Func<int, int> CreateAdder(int amount) => x => x + amount; var addFive = CreateAdder(5); var addTen = CreateAdder(10); Console.WriteLine(addFive(3)); // 8 Console.WriteLine(addTen(3)); // 13
The returned closure retains the amount value, so each returned delegate behaves independently. This technique is handy for building reusable building blocks such as validation rules, filters, or configuration-based transformations.
Func vs Action vs Predicate
Choosing the right delegate type depends on the return type and the number of parameters. Predicate<T> is a built-in delegate that returns bool and is used to test a condition. Its signature has the same shape as Func<T, bool>. The table below summarizes common delegate choices.
| Delegate Type | Signature | Return Type | Use Case |
|---|---|---|---|
Func<T, TResult> | T -> TResult | any value | transformation, projection |
Action<T> | T -> void | void | side effect |
Predicate<T> | T -> bool | bool | filtering, condition check |
If a method returns a boolean and you only need a test, Predicate<T> communicates intent more clearly than Func<T, bool> in APIs that expect a predicate. LINQ methods like Where take Func<T, bool>, not Predicate<T>, so choose the delegate to match the API you are calling. When either is accepted, the choice is mainly about readability.
Func for Lazy Initialization and Caching
A Func delegate is a natural fit for caching the result of an expensive operation. You can wrap a slow computation inside a Func and store the result after the first call.
class LazyValue<T> { private Func<T> _factory; private T? _value; private bool _computed; public LazyValue(Func<T> factory) => _factory = factory; public T GetValue() { if (!_computed) { _value = _factory(); _computed = true; } return _value!; } }
This is lazy initialization: the function is evaluated only once, and subsequent calls return the cached value. For a thread-safe version, use System.Lazy<T>. The delegate defers the execution until first use, which is a common pattern for expensive resource initialization or complex calculations.
Performance and Allocation Considerations
Func delegates are reference types, and each lambda expression may compile to a new delegate instance. In hot paths, this can cause extra allocations. However, if the lambda does not capture any variables, the compiler can cache a static delegate instance and reuse it, eliminating repeated allocations.
For example, a capturing lambda inside a loop can create a new closure and delegate each iteration because each iteration may need its own captured state. A non-capturing lambda such as x => x * 2 can be cached by the compiler and reused. In practice, the JIT can often inline delegate calls, so the overhead is negligible for most applications. If you are building a high-throughput library, you can cache a delegate in a static field to avoid recreating it.
private static readonly Func<int, int> Double = x => x * 2;
Measure with a profiler before optimizing; delegate allocation is rarely the bottleneck compared to I/O or database queries.
Common Pitfalls with Func Delegates
One frequent mistake is using a Func when the method actually returns void. The compiler rejects this because Func<T> requires a return value. Another issue is expecting the delegate to execute immediately. A Func does not run until you invoke it. If you need immediate execution, call the delegate at the point of creation, not later.
Also, be careful when passing a Func to a method that may invoke it after the original context has changed. Captured variables reflect the current state at invocation time, as shown earlier. This can cause unexpected results if the captured variable is a mutable loop variable or a property that changes after the delegate is created.
When to Use a Named Delegate Instead
While Func covers most scenarios, a named delegate type can improve readability when the same signature is used across many methods. For example, in a domain model, you might define a delegate to represent a validation function:
public delegate bool ValidationRule(string input);
This gives the delegate a semantic name and can be documented. However, it requires extra code and is often unnecessary. Choose a named delegate when the signature appears in multiple places and a descriptive name adds significant clarity. Otherwise, using Func keeps the code concise and follows common .NET conventions.
Func Usage in Async and Cancellation Scenarios
Async methods that return a value can be wrapped in Func<Task<T>>. This allows deferring the async operation and controlling when it starts.
Func<Task<string>> fetchData = async () => { await Task.Delay(100); return "data"; }; string result = await fetchData();
The delegate returns a Task<string>, and the caller can decide whether to await it immediately or combine it with other operations. This pattern is useful for lazy async initialization or for passing a factory that might start after some precondition is met.
Remember that a Func delegate itself is not async. The delegate simply returns a Task; the asynchronous behavior is determined by the implementation. If the caller needs to cancel the operation, the delegate should accept a CancellationToken as one of its parameters.
Func<CancellationToken, Task<string>> fetchData = async token => { await Task.Delay(100, token); return "data"; };
Passing the token into the delegate body respects cancellation and avoids blocking the thread.