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

  1. The compiler sees a variable or parameter such as Func<decimal, decimal> and knows the exact call signature required.
  2. When you assign a method group or lambda, the compiler checks that the parameter types and return type match the delegate.
  3. For a non-capturing lambda, the compiler can create a delegate to a generated static method or reuse a cached delegate in many cases.
  4. 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.
  5. At runtime, invoking the delegate calls its Invoke method, which dispatches to the stored method and target.
  6. If multiple delegates are combined, their invocation list runs in order. This is more common with Action than Func, 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 Func when the delegate returns a value, and remember that the return type is the final generic argument.
  • Use Action for callbacks that perform work and return void, 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 PaymentValidator or RetryPolicy.
  • Keep delegate signatures small. If a callback needs many parameters, pass a small request object or record instead.
  • Avoid hidden side effects in Func callbacks. 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

  1. Create a Func<string, int> that returns the length of a string. Use it to print the lengths of three names.
  2. Write a method named Repeat that accepts an int count and an Action<int>. It should call the action with values from 1 through count.
  3. Create a list of numbers and a Predicate<int> named isOdd. Print only the odd numbers from the list.

Summary

  • Func, Action, and Predicate are built-in generic delegate types.
  • Func returns a value, with the return type written last.
  • Action returns void and 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.