📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are Func, Action and Predicate in .NET? When do you use each, why can a Func<T, bool> not be passed where a Predicate<T> is expected, and when would you declare your own delegate type instead?

The built-in generic delegates Func, Action and Predicate: signatures, variance, where the BCL uses each, delegate type incompatibility and choosing custom delegates or expression trees.

Junior Theory Beginner action csharp delegates func predicate

🎯 Key points

  • `Func<..., TResult>` returns a value, with 0 to 16 input parameters; the last type argument is the result
  • `Action<...>` returns void, with 0 to 16 parameters
  • `Predicate<T>` is `bool (T)`, used by older APIs such as `List<T>.FindAll`, `Array.Find` and `RemoveAll`
  • Delegate types are nominal: `Func<T, bool>` and `Predicate<T>` are different types, so you must wrap one in the other
  • Inputs are contravariant (`in`) and Func's result is covariant (`out`)
  • Use `Expression<Func<...>>` when a provider (EF Core) needs to inspect the code rather than run it
  • Declare a custom delegate when you need ref/out/in parameters, a descriptive name, or documentation

⏱ 30-second answer

Func is a delegate that returns a value, so Func<int, string> takes an int and returns a string. Action returns void, and Predicate<T> is a delegate that takes a T and returns bool. Older APIs like List<T>.FindAll use it, while LINQ uses Func<T, bool>. Even with identical signatures they are different delegate types, so you cannot pass a Func<T, bool> variable as a Predicate<T> without wrapping it. A lambda converts to either. I declare my own delegate only when I need ref or out parameters or a meaningful name.

🗣 Full answer (senior)

How I would answer this in an interview

Func, Action and Predicate are built-in generic delegate types, so you rarely need to declare your own:

  • Action<...> — takes 0–16 parameters, returns void.
  • Func<..., TResult> — takes 0–16 parameters, returns TResult (the last type argument).
  • Predicate<T> — takes one T, returns bool (an older, specialised delegate from .NET 2.0).

Then the subtle point: delegate types are nominal, so a Func<T, bool> isn't convertible to a Predicate<T> even though the signatures match.

1. When to use each

Action log = () => Console.WriteLine("Started");
Action<string, LogLevel> write = (msg, level) => logger.Log(level, msg);

Func<DateTimeOffset> now = () => DateTimeOffset.UtcNow;
Func<Order, decimal> total = o => o.Lines.Sum(l => l.Amount);
Func<string, CancellationToken, Task<Customer?>> load = repo.FindByEmailAsync;

Predicate<Order> isOverdue = o => o.DueDate < today;
List<Order> overdue = orders.FindAll(isOverdue);   // List<T>.FindAll/RemoveAll/Exists take Predicate<T>
  • Action: callbacks and commands where you don't need a result (Parallel.ForEach, Task.Run(Action), configuration callbacks like services.Configure<Options>(o => ...)).
  • Func: factories, projections, selectors, async callbacks (Func<Task>), and anything returning a value — LINQ uses Func everywhere (Where(Func<T,bool>), Select(Func<T,TResult>)).
  • Predicate<T>: mostly when calling older BCL APIs that require it (List<T>.FindAll, RemoveAll, Array.Find, Array.Exists). In new APIs, prefer Func<T, bool> for consistency with LINQ.

Related built-ins: Comparison<T> (List<T>.Sort), Converter<TInput, TOutput> (ConvertAll), EventHandler<T> for events.

2. Why Func<T, bool> can't be passed as Predicate<T>

Func<Order, bool> f = o => o.Total > 100;
orders.FindAll(f);                         // CS1503: cannot convert Func<Order,bool> to Predicate<Order>

Delegate types are distinct named types (each is a class deriving from MulticastDelegate). C# has no structural conversion between delegate types with identical signatures — just as two different classes with the same members aren't interchangeable.

Fixes:

orders.FindAll(new Predicate<Order>(f));   // wrap (creates a new delegate pointing at f.Invoke)
orders.FindAll(f.Invoke);                  // method group conversion of Invoke
orders.FindAll(o => f(o));                 // lambda wrapper
orders.Where(f).ToList();                  // or use LINQ, which takes Func

A lambda or method group can convert to either type directly — the problem only arises once it's already stored in a variable of one delegate type.

3. When to declare your own delegate type

  • ref, out, in parameters, params, or pointer types — Func/Action can't express them: public delegate bool TryParser<T>(ReadOnlySpan<char> s, out T value);
  • Readability and documentation: a named type (delegate decimal DiscountRule(Order o)) can carry XML docs and a meaningful name in APIs, especially public library APIs.
  • More than 16 parameters (rare and a design smell).
  • Events with a specific, documented signature (though EventHandler<T> is the convention).
  • Interop callbacks for native code with [UnmanagedFunctionPointer] (or function pointers).
  • Ref struct parameters in older versions (C# 13/.NET 9 annotated Func/Action with allows ref struct, reducing this need).

Otherwise prefer Func/Action — every developer knows them, and they compose with LINQ and the BCL.

4. A production example

A validation library defined its own delegate bool Rule<T>(T value) and every consumer converted between Rule<T>, Predicate<T> and Func<T,bool> with wrapper lambdas — conversions sprinkled everywhere and occasional confusion about which overload to call. Standardising on Func<T, bool> in the public API (keeping Rule<T> as an obsolete overload during migration) removed the wrappers and made rules usable directly with LINQ.

🔍 What the interviewer is checking

Checks fluency with the built-in delegate types and awareness of the Func versus Expression trap.

Green flags: correct signatures; knows the last type argument is the result; explains nominal typing; brings up Expression<Func<>> with EF Core; reasons about custom delegates.

Red flags: thinks Predicate and Func<T,bool> are interchangeable variables; unaware of expression trees.

📚 Learn in depth

1. Definitions

public delegate void Action();
public delegate void Action<in T>(T obj);
// ... up to Action<in T1, ..., in T16>
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
// ... up to 16 inputs
public delegate bool Predicate<in T>(T obj);

All are variant: inputs in (contravariant), result out (covariant). In .NET 9 many were annotated with allows ref struct so spans can be type arguments.

2. Delegate conversion rules

  • Lambda/anonymous method → delegate type: allowed if the signature is compatible (with parameter/return variance for reference types).
  • Method group → delegate type: allowed if a compatible overload exists.
  • Delegate → different delegate type: not implicitly allowed, except variance conversions between instantiations of the same generic delegate (Func<string> → Func<object>).
  • Explicit construction new Predicate<T>(func) creates a delegate targeting func.Invoke, adding one level of indirection.

Since C# 10, lambdas have a natural type: var f = (Order o) => o.Total > 100; infers Func<Order, bool> — another reason Func is the de-facto standard.

3. Expression trees

Func<T, bool> lambdas can also be converted to Expression<Func<T, bool>> (used by IQueryable/EF Core). A variable of type Func<T,bool> can't become an expression tree — passing a compiled Func to EF Core's Where binds to Enumerable.Where (in-memory) instead of Queryable.Where, loading the whole table. This is a common EF performance bug.

4. Performance considerations

  • Delegate invocation is an indirect call (not inlined, except with PGO guarded devirtualisation).
  • Wrapping a delegate in another delegate (new Predicate<T>(f)) adds an extra call and allocation.
  • Capturing lambdas allocate closures; static lambdas avoid it.

5. Choosing in API design

API style Prefer
LINQ-like filtering/projection Func<T, bool>, Func<T, TResult>
Callback without result Action<T>
Async callback Func<T, CancellationToken, Task>
Try-pattern callbacks with out Custom delegate
Public library extension point with rich semantics Interface (multiple methods) or named delegate with docs

Try it yourself

  1. Store a lambda in Func<int,bool> and pass it to List<int>.FindAll; fix the error three ways.
  2. Use var f = (string s) => s.Length; and inspect its type.
  3. Declare delegate bool TryParser<T>(string s, out T value) and use it with int.TryParse.
  4. Pass a Func<Order,bool> variable to an EF Core query's Where and inspect the generated SQL (or lack of a WHERE clause).

💻 Code

using System.Linq.Expressions;

namespace Delegates.BuiltIn;

public sealed record Customer(int Id, string Name, bool IsActive, decimal Balance);

public delegate bool TryParser<T>(string input, out T value); // Func cannot express out

public static class Program
{
    public static void Main()
    {
        List<Customer> customers =
        [
            new(1, "Ava", true, 120m),
            new(2, "Noah", false, 0m),
            new(3, "Mia", true, -15m),
        ];

        Action<string> log = message => Console.WriteLine($"[log] {message}");
        Func<Customer, string> label = c => $"{c.Id}:{c.Name}";
        Predicate<Customer> inDebt = c => c.Balance < 0;
        Func<Customer, bool> isActive = c => c.IsActive;

        log(string.Join(", ", customers.Where(isActive).Select(label)));
        log($"In debt: {customers.FindAll(inDebt).Count}");

        // customers.FindAll(isActive);                   // CS1503: Func<Customer,bool> is not Predicate<Customer>
        log($"Active: {customers.FindAll(isActive.Invoke).Count}");   // method group conversion
        log($"Active: {customers.FindAll(new Predicate<Customer>(isActive)).Count}");

        Func<object, string> describe = o => o.ToString() ?? "";
        Func<Customer, object> asObject = describe;     // variance: in T, out TResult
        log(asObject(customers[0]).ToString()!);

        Expression<Func<Customer, bool>> activeExpr = c => c.IsActive; // data, for IQueryable providers
        log(activeExpr.Body.ToString());

        TryParser<int> parser = int.TryParse;
        log(parser("42", out var n) ? $"parsed {n}" : "failed");
    }
}

⚠ Common mistakes

### 1. Expecting structural delegate conversion
**What happens:** passing a `Func

🏛 Senior insight: trade-offs

The senior distinction is not memorising signatures; it is knowing when the delegate is code and when it must be data. Func<T, bool> versus Expression<Func<T, bool>> decides whether a query runs in SQL Server or in your pod's memory, and I have seen that one type cause production outages. I default to Func/Action in new APIs for interoperability, and use named delegates where names or ref parameters genuinely add clarity, as ASP.NET Core does with RequestDelegate.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate passes lambdas to FindAll or Where. Why: Lambdas are how Func/Action/Predicate are usually created.
    How to answer

    A lambda is an anonymous function, either an expression like x => x * 2 or a statement block. It has no type until it is converted, either to a delegate, which is compiled code, or to an Expression<Func<...>>, which is a data structure EF Core can turn into SQL. Since C# 10 a lambda can have a natural type, so var f = (int x) => x * 2 is a Func<int,int>. When it captures variables, the compiler hoists them into a closure class, which allocates. static lambdas forbid capture. Newer additions are attributes and return types (C# 10), default parameters (C# 12), and C# 14 parameter modifiers like out without writing types.

  2. When: Candidate says Func and Predicate are different types. Why: Explains why delegate types are nominal and what a delegate object holds.
    How to answer

    A delegate is a type-safe reference to a method. The delegate type defines the signature, and a delegate instance is an object that holds the method and, for instance methods, the target object, so calling Invoke calls that method on that object. Delegates are immutable reference types that derive from MulticastDelegate, and two delegates are equal if they point at the same method on the same target. Creating one allocates, though the compiler caches non-capturing lambdas. I use Func and Action for most callbacks, and an interface when the contract has more than one method.

↩ Usually asked after

More in Delegates and Events

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me