Delegates

Intro

A delegate is a type-safe function pointer in C#. It lets you treat methods as values: store them in variables, pass them to other methods, compose invocation lists, and invoke them later. Delegates are foundational for callbacks, LINQ, strategy-style APIs, and events.

A delegate type defines a method signature. Any method with a compatible signature (static or instance) can be assigned to that delegate variable.

public delegate decimal PriceCalculator(int quantity, decimal unitPrice);

public static decimal StandardPrice(int q, decimal p) => q * p;

PriceCalculator calc = StandardPrice;
var total = calc(3, 19.99m); // 59.97

Built-in generic delegates

Func<int, int, int> add = (a, b) => a + b;
Action<string> log = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;

Multicast Delegates

Delegates can hold an invocation list (+=, -=). Calling the delegate invokes handlers in registration order.

Action pipeline = () => Console.WriteLine("Step 1");
pipeline += () => Console.WriteLine("Step 2");
pipeline += () => Console.WriteLine("Step 3");

pipeline();

Important runtime behavior:

Variance

Delegates support covariance and contravariance:

class Animal { }
class Dog : Animal { }

Func<Dog> dogFactory = () => new Dog();
Func<Animal> animalFactory = dogFactory; // covariance

Action<Animal> inspectAnimal = a => Console.WriteLine(a.GetType().Name);
Action<Dog> inspectDog = inspectAnimal;   // contravariance

Anonymous Methods and Lambdas

Anonymous methods (delegate(...) { ... }) and lambdas ((...) => ...) compile to delegate instances. Both can capture local variables (closures).

int threshold = 10;
Func<int, bool> greaterThanThreshold = x => x > threshold;

Captured variables are references to closure state, not a one-time value copy.

Closures

A closure is the runtime state created when a lambda or anonymous method captures variables from an outer scope. The captured variable is shared, so updates to that variable are observed by all delegates that close over it.

var handlers = new List<Action>();

for (int i = 0; i < 3; i++)
{
    handlers.Add(() => Console.WriteLine(i));
}

handlers.ForEach(h => h()); // 3, 3, 3

Why this happens: the lambda captures the variable i, not its value per iteration.

var handlers = new List<Action>();

for (int i = 0; i < 3; i++)
{
    int copy = i; // capture per-iteration value
    handlers.Add(() => Console.WriteLine(copy));
}

handlers.ForEach(h => h()); // 0, 1, 2

Use this pattern when the captured variable would otherwise be shared and mutated after handler creation (most commonly for loop indices).

Pitfalls

Questions


Whats next