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
// 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.
// 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)); // positiveFunc<T, TResult> — Delegate That Returns a Value
Func accepts up to 16 input parameters and one return type (last type argument).
// 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, 10Action<T> — Delegate That Returns void
Use Action when the method performs a side effect and returns nothing.
// 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 / TickPredicate<T> — Specialized Func<T, bool>
Predicate<T> is shorthand for Func<T, bool> — commonly used with List.Find, List.FindAll, and List.RemoveAll.
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 removedClosures — Capturing Variables
Lambdas can capture variables from their enclosing scope. The lambda holds a reference, not a copy.
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
// 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.99Func vs Action vs Delegate — Quick Reference
| Type | Signature | Returns | When to use |
|---|---|---|---|
Action | () | void | Side-effect only, no return |
Action<T> | (T arg) | void | Side-effect, one input |
Func<TResult> | () | TResult | No input, returns a value |
Func<T, TResult> | (T arg) | TResult | One input, returns a value |
Predicate<T> | (T arg) | bool | Filter / test condition |
| Custom delegate | Any | Any | Multicast events, callbacks with events |