Delegates, Lambdas & Events - Functions as Values
Up to now, a method has been something you call by name. This phase flips that: a method can also be a value - handed to another method, stashed in a variable, called later. Once functions are first-class values, whole categories of code get shorter: sorting by a custom rule, retrying with a strategy, reacting when something happens.
The mental model: a function can be passed around like data. Delegates are the typed box you put a function in. Lambdas are the shortest way to write one on the spot. Func/Action/Predicate are the standard boxes everyone uses. Events are delegates with a safety rail bolted on. Get this clicking - Phase 12: LINQ is built entirely on top of it.
Delegates - a typed reference to a method
📝 A delegate is a type-safe reference to a method - a "function pointer, but with the parameter and return types checked by the compiler." Declaring a delegate type describes a shape of method: how many parameters, what types, what it returns. Any matching method can be stored in a variable of that delegate type and called through it.
A delegate type is declared like a method signature with the delegate keyword in front:
// Declare a delegate TYPE: "any method taking two ints and returning an int".
delegate int Op(int a, int b);
class Calculator
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
Op operation = Add; // store a method in a delegate variable
Console.WriteLine(operation(3, 4)); // call it - runs Add(3, 4)
operation = Multiply; // point the same variable at another method
Console.WriteLine(operation(3, 4)); // now runs Multiply(3, 4)
}
}
7
12
What just happened: delegate int Op(int a, int b); defined a new type named Op whose values are "methods that take two ints and return an int." Op operation = Add; stored the Add method itself - not its result. Calling operation(3, 4) ran whatever method the variable held; reassigning operation = Multiply swapped behavior without touching the call site. The compiler enforces the shape throughout - a wrong signature won't compile, the "type-safe" part a raw C function pointer lacks.
💡 Key point. The payoff isn't reassigning a variable - it's passing behavior into other code. A method can take an Op parameter and let the caller decide what operation to run: one sort routine that sorts by any rule, one retry loop that runs any action.
Lambdas & anonymous methods - functions written inline
Declaring a separate named method just to pass it somewhere is a lot of ceremony for one line of logic. A lambda is a function written inline, right where you need it, with no name.
📝 The syntax is parameters => body. The => reads as "goes to." A single-expression body returns its value automatically (an expression lambda). Multiple statements need braces and an explicit return (a statement lambda).
delegate int Op(int a, int b);
class Program
{
static void Main()
{
// Expression lambda: one expression, value returned implicitly.
Op add = (a, b) => a + b;
// Statement lambda: braces, multiple lines, explicit return.
Op maxPlusOne = (a, b) =>
{
int bigger = a > b ? a : b;
return bigger + 1;
};
Console.WriteLine(add(3, 4));
Console.WriteLine(maxPlusOne(3, 4));
}
}
7
5
What just happened: (a, b) => a + b created a function with no name, stored directly in an Op delegate - no separate static int Add(...) needed. The compiler inferred a and b as int from the Op type, so you didn't repeat them. The statement lambda used braces since it has more than one line, needing an explicit return. Both are the same idea as named methods, just written at the point of use.
(The older delegate (int a, int b) { return a + b; } form is an anonymous method - the pre-lambda way to write the same thing. Lambdas replaced it.)
Func, Action, Predicate - the built-in delegate types
Declaring a custom delegate type for every shape gets tedious, and worse, your Op and my Calc would be incompatible even with identical signatures. .NET ships a standard set of generic delegate types instead - learn these three and you can read almost any modern C# API.
📝 Func<…, TResult> - a method that returns a value; the last type parameter is the return type, the rest are parameters. Func<int, int, int> takes two ints and returns an int.
📝 Action<…> - a method that returns void. Action<string> takes a string and returns nothing.
📝 Predicate<T> - a method taking one T, returning bool - a yes/no test. Predicate<int> asks a true/false question about an int.
// Func: takes two ints, returns an int (return type is LAST).
Func<int, int, int> add = (a, b) => a + b;
// Action: takes a string, returns nothing.
Action<string> shout = msg => Console.WriteLine(msg.ToUpper());
// Predicate: takes an int, returns a bool - a test.
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(add(2, 5));
shout("hello");
Console.WriteLine(isEven(4));
7
HELLO
True
What just happened: Func<int, int, int> held a value-returning function - read the type parameters left to right as "int, int → int," return type always last. Action<string> held a function with no return value, only a side effect (printing). Predicate<int> held a true/false test. Note shout and isEven skip parentheses around a single parameter - msg => … is shorthand for (msg) => …. Each is exactly a delegate, just pre-declared so you never write delegate yourself.
💡 Key point. These three are the vocabulary LINQ and modern APIs speak: .Where(...) wants a Func<T, bool> (a test), .Select(...) a Func<T, TResult> (a transform), list.ForEach(...) an Action<T>. A parameter typed Func or Action wants you to hand it behavior - usually a lambda.
Closures - a lambda that captures its surroundings
📝 A closure is what you get when a lambda uses a variable from the scope where it was defined. The lambda doesn't copy the value - it captures the variable itself, reading its current value whenever it runs, even long after the surrounding method has returned.
Func<int, int> MakeAdder(int amount)
{
// The returned lambda "captures" the parameter `amount`.
return x => x + amount;
}
var add10 = MakeAdder(10);
var add100 = MakeAdder(100);
Console.WriteLine(add10(5)); // 15
Console.WriteLine(add100(5)); // 105
15
105
What just happened: MakeAdder returned a lambda referring to amount, a local parameter that would normally vanish when MakeAdder returns - but the lambda captured it, keeping it alive bundled with the function. add10 and add100 each closed over their own amount, which is why they behave differently.
⚠️ Gotcha - capturing a loop variable. Because closures capture the variable, not a snapshot, capturing a for loop's counter bites hard - every lambda shares the one loop variable and sees its final value:
var funcs = new List<Func<int>>();
// for loop: ONE shared `i` captured by all three lambdas.
for (int i = 0; i < 3; i++)
funcs.Add(() => i);
foreach (var f in funcs)
Console.WriteLine(f()); // prints 3, 3, 3 - not 0, 1, 2!
// foreach: modern C# gives each iteration its OWN variable.
var fixedFuncs = new List<Func<int>>();
foreach (var n in new[] { 0, 1, 2 })
fixedFuncs.Add(() => n);
foreach (var f in fixedFuncs)
Console.WriteLine(f()); // prints 0, 1, 2 - fresh `n` each time
3
3
3
0
1
2
What just happened: the for loop has a single variable i that lives for the whole loop; all three lambdas captured that one variable, and by the time you called them the loop had finished, leaving i at 3. foreach is different: modern C# (5+) gives each iteration a fresh loop variable, so each lambda captured its own n. Fix the for case by copying into a local inside the loop: int copy = i; funcs.Add(() => copy); - now each lambda captures a distinct copy. ⚠️ foreach was fixed; for still bites.
Events - publish/subscribe built on delegates
📝 An event is a publish/subscribe mechanism built on delegates. One object (the publisher) announces "something happened"; other objects (subscribers) register to be notified, attaching with += and detaching with -=. The publisher raises the event to call everyone at once - the backbone of UI toolkits and reactive code, where a button doesn't know who's listening for its click, it just fires.
class Button
{
// An event: subscribers attach handlers; only Button can raise it.
public event EventHandler? Clicked;
public void SimulateClick()
{
Console.WriteLine("Button: raising Clicked");
Clicked?.Invoke(this, EventArgs.Empty); // notify every subscriber
}
}
class Program
{
static void Main()
{
var button = new Button();
// Subscribe two handlers with +=.
button.Clicked += (sender, e) => Console.WriteLine("Handler A: clicked!");
button.Clicked += (sender, e) => Console.WriteLine("Handler B: also clicked!");
button.SimulateClick();
}
}
Button: raising Clicked
Handler A: clicked!
Handler B: also clicked!
What just happened: public event EventHandler? Clicked; declared an event - EventHandler is the standard delegate type for "something happened" notifications (it carries a sender and an EventArgs). Two subscribers attached lambdas with +=. SimulateClick raised the event with Clicked?.Invoke(...) - the ?. guards against nobody having subscribed (then Clicked is null and invoking it would throw). Button never named or knew about either handler - that decoupling is the point.
💡 Key point. An event is "a delegate with subscribe/unsubscribe semantics and safety." Under the hood it's a delegate holding a list of methods, but the event keyword restricts the outside world to only += and -=: subscribers can add or remove their own handler, but can't overwrite the whole list, clear everyone else's, or raise the event themselves. Only the declaring class can do that - why events, not raw public delegates, model "X happened, react if you care."
Recap
- A delegate is a type-safe reference to a method - a typed box for a function, pass around, call later.
delegate int Op(int a, int b);declares the shape; any matching method fits. - Lambdas write functions inline:
(a, b) => a + bfor a single expression (returned implicitly), or(a, b) => { … return x; }with braces for multiple statements. Func,Action,Predicateare the built-in delegate types everyone uses:Func<…,TResult>returns a value,Action<…>returns void,Predicate<T>returns bool - the vocabulary LINQ speaks.- A closure captures the variable from its enclosing scope (not a snapshot), keeping it alive. ⚠️ Capturing a
forloop variable makes every lambda share one variable and see its final value;foreachwas fixed in modern C#, butforstill bites - copy to a local inside the loop. - An event is publish/subscribe on top of delegates: subscribe with
+=, unsubscribe with-=, raise with?.Invoke(...). Theeventkeyword adds safety so only the declaring class can raise or replace the handler list.
You can now treat functions as values - store, pass, react. That's the foundation Phase 12: LINQ stands on: every query operator takes a function (a lambda) and applies it to a sequence.
Quick check
Test yourself on the ideas that LINQ will lean on hardest:
[
{
"q": "What is a delegate in C#?",
"choices": [
"A type-safe reference to a method - you can store a method in it, pass it around, and call it later",
"A keyword that makes a method run on a background thread",
"A way to inherit behavior from another class",
"A read-only property that can't be reassigned"
],
"answer": 0,
"explain": "A delegate is a typed handle to a method - a 'function pointer with types.' You declare a shape (parameters and return type), store any matching method in a variable of that type, and call behavior through it. That's what lets functions be passed around as values."
},
{
"q": "You want to pass a lambda that takes an `int` and returns a `bool` (a test). Which built-in delegate type fits?",
"choices": [
"`Func<int, bool>` (or `Predicate<int>`) - it takes an int and returns a bool",
"`Action<int>` - it takes an int",
"`Func<bool, int>` - bool first, then int",
"`Op<int>` - the standard test delegate"
],
"answer": 0,
"explain": "`Func<int, bool>` reads 'int → bool': parameters first, return type last. `Predicate<int>` is the same shape. `Action<int>` is wrong because Action returns void. This is exactly the shape LINQ's `.Where(...)` expects."
},
{
"q": "You add three lambdas `() => i` inside a `for (int i = 0; i < 3; i++)` loop, then call them all. What prints?",
"choices": [
"3, 3, 3 - all three lambdas captured the same `i`, which ended at 3",
"0, 1, 2 - each lambda captured the value of `i` at that iteration",
"0, 0, 0 - the lambdas captured `i` before the loop ran",
"A compile error - you can't capture a loop variable"
],
"answer": 0,
"explain": "A closure captures the variable, not a snapshot. A `for` loop has one shared `i`, so all three lambdas point at it; by the time you call them the loop is done and `i` is 3. Copy to a local inside the loop (`int copy = i;`) to fix it. Note `foreach` was fixed in modern C# to give each iteration its own variable."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. What is a delegate in C#?
2. You want to pass a lambda that takes an `int` and returns a `bool` (a test). Which built-in delegate type fits?
3. You add three lambdas `() => i` inside a `for (int i = 0; i < 3; i++)` loop, then call them all. What prints?