Lesson 158 min read

Delegates, Events & Lambdas

Pass behavior around like data — the secret sauce of modern C#

What Is a Delegate?

Normally, variables hold data — numbers, strings, objects. But what if you could store a method in a variable? That's exactly what a delegate does.

Think of a delegate as a TV remote control. The remote doesn't know which TV it's pointed at — it just knows "when I press the button, something happens." You can point it at different TVs (different methods), and the button press triggers whatever is currently connected.

This is incredibly powerful because it lets you write flexible code. Instead of hard-coding which method to call, you pass the method as a parameter. The calling code decides what happens.

Delegates — Methods as Variables

// Define a delegate type (the "shape" of methods it can hold)
delegate int MathOperation(int a, int b);
// Methods that match the delegate's shape
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static int Max(int a, int b) => a > b ? a : b;
// Use the delegate like a variable
MathOperation op = Add;
Console.WriteLine($"Add: {op(10, 5)}");
op = Multiply; // point at a different method
Console.WriteLine($"Multiply: {op(10, 5)}");
op = Max;
Console.WriteLine($"Max: {op(10, 5)}");
// Pass a delegate to another method
static int ApplyToArray(int[] numbers, MathOperation operation)
{
int result = numbers[0];
for (int i = 1; i < numbers.Length; i++)
result = operation(result, numbers[i]);
return result;
}
int[] nums = { 3, 7, 2, 9, 4 };
Console.WriteLine($"\nSum of all: {ApplyToArray(nums, Add)}");
Console.WriteLine($"Product: {ApplyToArray(nums, Multiply)}");
Console.WriteLine($"Largest: {ApplyToArray(nums, Max)}");
Output
Add: 15
Multiply: 50
Max: 10

Sum of all: 25
Product: 1512
Largest: 9

Action<T> and Func<T,TResult> — Built-in Delegates

Defining your own delegate types gets tedious. C# provides two built-in generic delegates that cover almost every case:

  • Action<T> — a method that takes parameters but returns nothing (void). Action<string> takes a string, returns void.
  • Func<T, TResult> — a method that takes parameters and returns a value. The last type is always the return type. Func<int, int, int> takes two ints and returns an int.

You'll rarely need to write your own delegate type anymore — Action and Func handle 99% of cases.

Action, Func & Lambda Expressions

// Action — takes input, returns nothing
Action<string> shout = message => Console.WriteLine(message.ToUpper() + "!!!");
shout("hello");
shout("watch out");
// Func — takes input, returns output
Func<int, int, int> add = (a, b) => a + b;
Func<string, int> wordCount = text => text.Split(' ').Length;
Func<double, double> toCelsius = f => (f - 32) * 5.0 / 9.0;
Console.WriteLine($"3 + 4 = {add(3, 4)}");
Console.WriteLine($"Words: {wordCount("The quick brown fox")}");
Console.WriteLine($"72°F = {toCelsius(72):F1}°C");
// Lambda with multiple statements (use { })
Func<int, string> classify = n =>
{
if (n > 0) return "positive";
if (n < 0) return "negative";
return "zero";
};
Console.WriteLine($"5 is {classify(5)}");
Console.WriteLine($"-3 is {classify(-3)}");
Console.WriteLine($"0 is {classify(0)}");
// Passing lambdas directly (no variable needed)
var numbers = new List<int> { 5, 12, 8, 3, 17, 9 };
var big = numbers.Where(n => n > 10).ToList();
Console.WriteLine($"\nBigger than 10: [{string.Join(", ", big)}]");
Output
HELLO!!!
WATCH OUT!!!
3 + 4 = 7
Words: 4
72°F = 22.2°C
5 is positive
-3 is negative
0 is zero

Bigger than 10: [12, 17]

Events — The Notification System

An event is like a newsletter subscription. A publisher (like a Button) says "I have a Click event." Any number of subscribers can sign up with +=. When the event fires, ALL subscribers get notified.

Events use delegates under the hood, but they add restrictions: subscribers can only += (subscribe) or -= (unsubscribe). They can't fire the event or replace other subscribers. This keeps things safe.

Events are the backbone of UI programming (button clicks, form submissions) and many design patterns (observer pattern, pub/sub). Under the hood, event systems often use a queue to process notifications in order.

Events — Subscribe, Publish, React

class TemperatureSensor
{
// Event that fires when temperature changes
public event Action<double>? TemperatureChanged;
public event Action<double>? OverheatWarning;
private double _temp;
public void SetTemperature(double newTemp)
{
_temp = newTemp;
Console.WriteLine($"Sensor: Temperature is now {_temp}°C");
// Fire the event — notify all subscribers
TemperatureChanged?.Invoke(_temp);
if (_temp > 100)
OverheatWarning?.Invoke(_temp);
}
}
// Create sensor
var sensor = new TemperatureSensor();
// Subscribe to events with lambdas
sensor.TemperatureChanged += temp =>
Console.WriteLine($" Logger: Recorded {temp}°C");
sensor.TemperatureChanged += temp =>
Console.WriteLine($" Display: Showing {temp}°C on screen");
sensor.OverheatWarning += temp =>
Console.WriteLine($" ALARM: DANGER! {temp}°C is too hot!");
// Simulate temperature changes
sensor.SetTemperature(22.5);
Console.WriteLine();
sensor.SetTemperature(75.0);
Console.WriteLine();
sensor.SetTemperature(105.3);
Output
Sensor: Temperature is now 22.5°C
  Logger: Recorded 22.5°C
  Display: Showing 22.5°C on screen

Sensor: Temperature is now 75°C
  Logger: Recorded 75°C
  Display: Showing 75°C on screen

Sensor: Temperature is now 105.3°C
  Logger: Recorded 105.3°C
  Display: Showing 105.3°C on screen
  ALARM: DANGER! 105.3°C is too hot!

Real-World: Callbacks & Higher-Order Methods

// Higher-order method: takes a function as a parameter
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> Map<TIn, TOut>(List<TIn> items, Func<TIn, TOut> transform)
{
var result = new List<TOut>();
foreach (var item in items)
result.Add(transform(item));
return result;
}
// Use with different lambdas — same method, different behavior!
var names = new List<string> { "Alice", "Bob", "Charlie", "Diana", "Eve" };
var longNames = Filter(names, n => n.Length > 3);
var shouted = Map(names, n => n.ToUpper());
var lengths = Map(names, n => n.Length);
Console.WriteLine($"Long names: [{string.Join(", ", longNames)}]");
Console.WriteLine($"Shouted: [{string.Join(", ", shouted)}]");
Console.WriteLine($"Lengths: [{string.Join(", ", lengths)}]");
// Callback pattern: "call me when you're done"
static void DownloadData(string url, Action<string> onComplete)
{
// Simulate download
string data = $"Data from {url}";
Console.WriteLine($"Downloading {url}...");
onComplete(data); // call the callback!
}
DownloadData("api.example.com/users", result =>
{
Console.WriteLine($"Got: {result}");
});
Output
Long names: [Alice, Charlie, Diana]
Shouted:    [ALICE, BOB, CHARLIE, DIANA, EVE]
Lengths:    [5, 3, 7, 5, 3]
Downloading api.example.com/users...
Got: Data from api.example.com/users
Note: 🔑 Lambda cheat sheet: (x) => x * 2 means "take x, return x times 2." The => arrow reads as "goes to" or "becomes." No parameters? Use () => doSomething(). Multiple params? Use (a, b) => a + b. Multiple statements? Use (x) => { ...; return result; }.

Quick check

What's the difference between Action and Func?
File I/O