Delegates, Events, and Lambdas in C#
Action, Func, Predicate, lambda expressions, multicast delegates, and events in C#.
Delegates
A delegate is a type-safe function pointer — a variable that holds a reference to a method.
// Declare a delegate type
delegate int MathOperation(int a, int b);
// Methods that match the signature
int Add(int a, int b) => a + b;
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
// Multicast delegate — combine methods with +
MathOperation combined = Add;
combined += Multiply;
combined(3, 4); // calls Add then Multiply (return value is from last)
Built-in Delegate Types
Instead of declaring custom delegate types, use the built-in ones:
// Action — no return value
Action greet = () => Console.WriteLine("Hello!");
Action<string> greetName = name => Console.WriteLine($"Hello, {name}!");
Action<string, int> greetAge = (name, age) => Console.WriteLine($"{name} is {age}");
greet();
greetName("Alice");
greetAge("Bob", 30);
// Func — returns a value (last type arg = return type)
Func<int> getZero = () => 0;
Func<int, int> square = x => x * x;
Func<int, int, int> add = (a, b) => a + b;
Func<string, int> strlen = s => s.Length;
Func<string, bool> isLong = s => s.Length > 10;
Console.WriteLine(square(5)); // 25
Console.WriteLine(add(3, 4)); // 7
// Predicate<T> — shorthand for Func<T, bool>
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // True
Lambda Expressions
Lambdas are inline anonymous functions:
// Expression lambda (single expression)
Func<int, int> square = x => x * x;
// Statement lambda (multiple statements)
Func<int, int> factorial = n =>
{
if (n <= 1) return 1;
int result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
};
// Lambda with no parameters
Action sayHi = () => Console.WriteLine("Hi!");
// Lambda capturing outer variables (closure)
int multiplier = 3;
Func<int, int> triple = x => x * multiplier; // captures 'multiplier'
Console.WriteLine(triple(7)); // 21
multiplier = 10;
Console.WriteLine(triple(7)); // 70 — captures the VARIABLE, not the value
Static lambdas (C# 9)
Mark a lambda static to prevent it from capturing outer variables accidentally:
Func<int, int> doubler = static x => x * 2;
// static x => x * multiplier ← compile error: can't capture 'multiplier'
Passing Delegates to Methods
Delegates make methods flexible and reusable:
// Higher-order function — takes a delegate
static List<T> Filter<T>(List<T> items, Func<T, bool> predicate)
{
var result = new List<T>();
foreach (var item in items)
if (predicate(item)) result.Add(item);
return result;
}
static List<TOut> Transform<TIn, TOut>(List<TIn> items, Func<TIn, TOut> selector)
=> items.Select(selector).ToList();
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evens = Filter(numbers, n => n % 2 == 0);
var squared = Transform(numbers, n => n * n);
var strings = Transform(numbers, n => $"Item {n}");
Multicast Delegates
Action<string> logger = msg => Console.WriteLine($"[LOG] {msg}");
Action<string> audit = msg => Console.WriteLine($"[AUDIT] {msg}");
// Combine into a multicast delegate
Action<string> combined = logger + audit;
combined("User logged in");
// [LOG] User logged in
// [AUDIT] User logged in
// Remove a handler
combined -= audit;
combined("User logged out");
// [LOG] User logged out
// Inspect invocation list
foreach (Action<string> handler in combined.GetInvocationList().Cast<Action<string>>())
Console.WriteLine(handler.Method.Name);
Events
Events are multicast delegates with restricted access — only the declaring class can invoke them:
public class Button
{
// Declare an event using EventHandler or a custom delegate
public event EventHandler? Clicked;
public event EventHandler<string>? HoverChanged;
// The class raises its own events
protected virtual void OnClicked()
=> Clicked?.Invoke(this, EventArgs.Empty);
protected virtual void OnHoverChanged(string state)
=> HoverChanged?.Invoke(this, state);
public void SimulateClick() => OnClicked();
public void SimulateHover(string state) => OnHoverChanged(state);
}
var button = new Button();
// Subscribe (+=)
button.Clicked += (sender, args) => Console.WriteLine("Button clicked!");
button.Clicked += (sender, args) => Console.WriteLine("Also handled!");
// Subscribe with a named method
void LogClick(object? sender, EventArgs e)
=> Console.WriteLine($"Logged: click on {sender?.GetType().Name}");
button.Clicked += LogClick;
button.SimulateClick();
// Button clicked!
// Also handled!
// Logged: click on Button
// Unsubscribe (-=)
button.Clicked -= LogClick;
Custom EventArgs
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
=> (OrderId, Amount) = (orderId, amount);
}
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderPlaced;
public event EventHandler<OrderEventArgs>? OrderCancelled;
protected virtual void OnOrderPlaced(int id, decimal amount)
=> OrderPlaced?.Invoke(this, new OrderEventArgs(id, amount));
public async Task PlaceOrderAsync(Order order)
{
// ... process order ...
OnOrderPlaced(order.Id, order.Total);
}
}
var service = new OrderService();
service.OrderPlaced += (_, e) =>
Console.WriteLine($"Order {e.OrderId} placed: ${e.Amount}");
Func as a Strategy Pattern
public class DataProcessor
{
private readonly Func<string, string> _transform;
private readonly Func<string, bool> _filter;
public DataProcessor(
Func<string, string> transform,
Func<string, bool> filter)
{
_transform = transform;
_filter = filter;
}
public IEnumerable<string> Process(IEnumerable<string> input)
=> input.Where(_filter).Select(_transform);
}
// Build different processors by injecting different lambdas
var upperCaseFilter = new DataProcessor(
transform: s => s.ToUpper(),
filter: s => s.Length > 3);
var results = upperCaseFilter.Process(new[] { "hi", "hello", "world", "c#" });
// HELLO, WORLD
Delegate Caching for Performance
In hot paths, avoid creating new delegates on every call:
public class OrderValidator
{
// Cache the delegate — don't allocate a new lambda on every call
private static readonly Func<Order, bool> IsValidOrder =
order => order.Lines.Count > 0 && order.Total > 0;
public bool Validate(Order order) => IsValidOrder(order);
} Frequently Asked Questions
What is the difference between Action and Func?
Action is a delegate that returns void. Func is a delegate that returns a value — the last type argument is the return type. Func<int, string> takes an int and returns a string.
What is a multicast delegate?
A delegate that holds a reference to more than one method. When you invoke it, all methods in the invocation list are called in order. Events use multicast delegates — multiple handlers can subscribe to one event.
When should I use events instead of a Func or Action delegate?
Use events for the publisher-subscriber pattern where the publisher doesn't know (or care) who's listening. Events prevent subscribers from invoking or replacing the delegate — only the declaring class can raise it. Use a Func/Action field when you want a single replaceable callback.