Delegates & Lambda
A delegate is a type-safe function pointer — an object that holds a reference to one or more methods matching a specific signature. Delegates are the foundation of events, callbacks, and higher-order functions in C#.
Declaring and Using Delegates
You declare a delegate type with the delegate keyword, specifying the return type and parameter list. Any method (static or instance) whose signature matches can be assigned to a variable of that delegate type.
public delegate int Transformer(int input);
Transformer square = x => x * x;
Console.WriteLine(square(5)); // 25Multicast Delegates
A single delegate variable can hold references to multiple methods. Use += to attach and -= to detach. When invoked, all attached methods run in the order they were added. The return value of a multicast delegate is the return value of the last method in the chain (earlier return values are discarded), so multicast is most common with void-returning delegates.
Action log = () => Console.WriteLine("Logger 1");
log += () => Console.WriteLine("Logger 2");
log(); // both runBuilt-In Generic Delegates
Instead of declaring custom delegate types, use the three generic families provided by the BCL:
| Type | Signature | Use case |
|---|---|---|
Action<T1,...> | void return | Side-effect callbacks |
Func<T1,...,TResult> | non-void return | Transformations, projections |
Predicate<T> | bool return | Filtering, conditions |
Lambda Expressions
Lambdas are the most concise way to create delegate instances. An expression lambda (x => x * x) has an implicit return. A statement lambda (x => { ... return ...; }) allows multiple statements. Lambdas can capture variables from the enclosing scope (closures).
Anonymous Methods vs Lambdas
Anonymous methods (delegate(int x) { return x * x; }) are the older syntax introduced in C# 2. Lambdas (C# 3+) are shorter and support expression trees. Prefer lambdas in all new code.
Delegate.Combine
Under the hood, += calls Delegate.Combine, which creates a new multicast delegate. -= calls Delegate.Remove. Both return a new delegate object — delegates are immutable.
Code Examples
using System;
// Custom delegate type
public delegate void Notifier(string message);
class Program
{
static void LogToConsole(string msg) =>
Console.WriteLine(````````[Console] ${msg}````````);
static void LogToFile(string msg) =>
Console.WriteLine(````````[File] ${msg}````````); // simulated
static void Main()
{
Notifier notifier = LogToConsole;
notifier += LogToFile; // attach second method
Console.WriteLine("--- Invoking multicast delegate ---");
notifier("System started");
Console.WriteLine();
notifier -= LogToFile; // detach
Console.WriteLine("--- After removing LogToFile ---");
notifier("System ready");
// GetInvocationList shows attached methods
Console.WriteLine(````````
Invocation list count: ${notifier.GetInvocationList().Length}````````);
}
}+= adds methods to the delegate's invocation list. -= removes them. GetInvocationList() lets you inspect all currently attached methods.
using System;
using System.Collections.Generic;
// Action<T> — void return, useful for side effects
Action<string> print = msg => Console.WriteLine(````````> ${msg}````````);
// Func<TInput, TOutput> — returns a value
Func<int, int> square = x => x * x;
Func<int, int> doubled = x => x * 2;
// Compose two Func delegates
Func<int, int> squareThenDouble = x => doubled(square(x));
print(````````square(4) = ${square(4)}````````);
print(````````doubled(4) = ${doubled(4)}````````);
print(````````squareThenDouble(4) = ${squareThenDouble(4)}````````);
// Func used as a higher-order parameter
static List<T> Transform<T>(List<T> list, Func<T, T> transform)
{
var result = new List<T>();
foreach (var item in list)
result.Add(transform(item));
return result;
}
var nums = new List<int> { 1, 2, 3, 4, 5 };
var squared = Transform(nums, x => x * x);
Console.WriteLine(````````Squared list: ${string.Join(", ", squared)}````````);Action<T> and Func<T,TResult> are built-in generic delegates that eliminate the need to declare custom types. Lambda expressions provide a concise syntax for creating delegate instances inline.
using System;
using System.Collections.Generic;
// Predicate<T> — returns bool, used for filtering
Predicate<int> isEven = n => n % 2 == 0;
Predicate<int> isPositive = n => n > 0;
// Combine predicates: both must be true
Predicate<int> isEvenAndPositive = n => isEven(n) && isPositive(n);
// A generic filter method using Predicate<T>
static List<T> Filter<T>(IEnumerable<T> source, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in source)
if (predicate(item))
result.Add(item);
return result;
}
var numbers = new List<int> { -4, -1, 0, 1, 2, 3, 4, 5, 6 };
var evens = Filter(numbers, isEven);
var positiveEvens = Filter(numbers, isEvenAndPositive);
var bigOdds = Filter(numbers, n => n > 2 && n % 2 != 0);
Console.WriteLine(````````Evens : ${string.Join(", ", evens)}````````);
Console.WriteLine(````````Positive evens : ${string.Join(", ", positiveEvens)}````````);
Console.WriteLine(````````Big odd nums : ${string.Join(", ", bigOdds)}````````);Predicate<T> is equivalent to Func<T, bool> and is used throughout the BCL for filtering. Composing predicates with logical operators lets you build complex conditions from simple ones.
Quick Quiz
1. What happens when you invoke a multicast delegate that has a non-void return type?
2. Which built-in generic delegate type should you use for a method that takes two ints and returns a bool?
3. What does the += operator do when applied to a delegate variable?
Was this lesson helpful?