Delegates, lambdas, Func, and Action in C#

A delegate is a type-safe function pointer — it holds a reference to a method with a specific signature. Lambdas are anonymous methods written inline using the => syntax. Func, Action, and Predicate are built-in generic delegate types that cover the most common signatures so you rarely need to declare your own delegate.

Declaring and Using a Delegate

C# Example Code
// Declare a delegate type — a signature contract
delegate int MathOperation(int a, int b);

// Method that matches the delegate signature
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;

// Assign and invoke
MathOperation op = Add;
Console.WriteLine(op(3, 4));   // 7

op = Multiply;
Console.WriteLine(op(3, 4));   // 12

// Delegates are multicast — combine multiple methods
MathOperation both = Add;
both += Multiply;
both(3, 4); // Invokes Add(3,4) then Multiply(3,4)

Lambda Expressions

A lambda is an anonymous function written inline. It replaces a named method when the logic is short and used only once.

C# Example Code
// Full delegate instantiation — verbose
Func<int, int, int> add1 = delegate(int a, int b) { return a + b; };

// Lambda — concise
Func<int, int, int> add2 = (a, b) => a + b;

// Single parameter — parentheses optional
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 25

// No parameters
Action greet = () => Console.WriteLine("Hello!");
greet(); // Hello!

// Multi-statement lambda — use braces and explicit return
Func<int, string> classify = n =>
{
    if (n < 0)  return "negative";
    if (n == 0) return "zero";
    return "positive";
};
Console.WriteLine(classify(-3));  // negative
Console.WriteLine(classify(0));   // zero
Console.WriteLine(classify(7));   // positive

Func<T, TResult> — Delegate That Returns a Value

Func accepts up to 16 input parameters and one return type (last type argument).

C# Example Code
// Func<TResult> — no input, returns a value
Func<DateTime> now = () => DateTime.UtcNow;
Console.WriteLine(now());

// Func<T, TResult> — one input, one return
Func<string, int> length = s => s.Length;
Console.WriteLine(length("hello")); // 5

// Func<T1, T2, TResult> — two inputs
Func<double, double, double> hypotenuse =
    (a, b) => Math.Sqrt(a * a + b * b);
Console.WriteLine(hypotenuse(3, 4)); // 5

// Use Func as a parameter — flexible, testable
static List<T> Filter<T>(List<T> items, Func<T, bool> predicate)
    => items.Where(predicate).ToList();

var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evens = Filter(numbers, n => n % 2 == 0);
Console.WriteLine(string.Join(", ", evens)); // 2, 4, 6, 8, 10

Action<T> — Delegate That Returns void

Use Action when the method performs a side effect and returns nothing.

C# Example Code
// Action — no input, no return
Action printLine = () => Console.WriteLine("---");
printLine(); // ---

// Action<T> — one input, no return
Action<string> log = message =>
    Console.WriteLine($"[LOG] {message}");

log("Application started"); // [LOG] Application started
log("Processing complete"); // [LOG] Processing complete

// Action<T1, T2>
Action<string, ConsoleColor> colorPrint = (text, color) =>
{
    Console.ForegroundColor = color;
    Console.WriteLine(text);
    Console.ResetColor();
};
colorPrint("Warning!", ConsoleColor.Yellow);

// Pass Action as a parameter — callback pattern
static void Repeat(int times, Action action)
{
    for (int i = 0; i < times; i++)
        action();
}

Repeat(3, () => Console.WriteLine("Tick")); // Tick / Tick / Tick

Predicate<T> — Specialized Func<T, bool>

Predicate<T> is shorthand for Func<T, bool> — commonly used with List.Find, List.FindAll, and List.RemoveAll.

C# Example Code
var names = new List<string> { "Alice", "Bob", "Charlie", "Anna", "David" };

// Predicate<string>
Predicate<string> startsWithA = name => name.StartsWith("A");

string?      first  = names.Find(startsWithA);      // Alice
List<string> all    = names.FindAll(startsWithA);    // [Alice, Anna]
int          removed = names.RemoveAll(startsWithA); // 2 removed

Console.WriteLine(first);                             // Alice
Console.WriteLine(string.Join(", ", all));            // Alice, Anna
Console.WriteLine($"{removed} names removed");        // 2 names removed

Closures — Capturing Variables

Lambdas can capture variables from their enclosing scope. The lambda holds a reference, not a copy.

C# Example Code
int threshold = 5;

Func<int, bool> isAbove = n => n > threshold;

Console.WriteLine(isAbove(3));  // False
Console.WriteLine(isAbove(7));  // True

// The lambda captures the variable — changing threshold affects future calls
threshold = 10;
Console.WriteLine(isAbove(7));  // False — threshold is now 10

// Common gotcha: capturing loop variable
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    int copy = i; // capture a copy, not the loop variable
    actions.Add(() => Console.WriteLine(copy));
}
actions.ForEach(a => a()); // 0 / 1 / 2  (not 3, 3, 3)

Storing and Passing Delegates as Parameters

C# Example Code
// Strategy pattern — swap algorithms at runtime
static double ApplyDiscount(decimal price, Func<decimal, decimal> discountStrategy)
    => (double)discountStrategy(price);

Func<decimal, decimal> tenPercent     = p => p * 0.90m;
Func<decimal, decimal> flatFiveOff    = p => p - 5.00m;
Func<decimal, decimal> noDiscount     = p => p;

decimal price = 49.99m;
Console.WriteLine(ApplyDiscount(price, tenPercent));   // 44.991
Console.WriteLine(ApplyDiscount(price, flatFiveOff));  // 44.99
Console.WriteLine(ApplyDiscount(price, noDiscount));   // 49.99

Func vs Action vs Delegate — Quick Reference

TypeSignatureReturnsWhen to use
Action()voidSide-effect only, no return
Action<T>(T arg)voidSide-effect, one input
Func<TResult>()TResultNo input, returns a value
Func<T, TResult>(T arg)TResultOne input, returns a value
Predicate<T>(T arg)boolFilter / test condition
Custom delegateAnyAnyMulticast events, callbacks with events