C# · Intermediate

Delegates and Events

A delegate is a variable that holds a method. An event is a delegate other classes can subscribe to — “call me when the water boils.”

Intermediate 6 min Lesson 19 of 26Examples target C# 13 on .NET 9 unless stated otherwise.
0/26 in C#
On this page · 6 min · Intermediate

In simple words#

A delegate is a phone number you can save and call later. An event is a doorbell: when someone presses it, everyone who subscribed hears it.

Delegate
A variable that points to a method.
Event
A notification others can subscribe to.
+= / -=
Subscribe / unsubscribe a handler.

Learning objectives#

  • Declare and call a delegate
  • Use the built-in Func and Action delegates
  • Pass a method as an argument
  • Publish and subscribe to an event

Explanation#

A delegate type describes a method signature: delegate int MathOp(int a, int b);. Any method with that shape can be stored in a MathOp variable and called later.

You rarely declare your own: Func<T1, T2, TResult> covers methods that return a value (the last type is the return type) and Action<T> covers methods that return void.

Passing a method as an argument lets the caller decide part of the behaviour: Apply(10, 5, Subtract).

An event (public event EventHandler<int>? Boiled;) is a delegate with protection: outside code can only += (subscribe) or -= (unsubscribe). Only the owning class can raise it.

Raise an event with Boiled?.Invoke(this, value). The ?. skips the call when nobody has subscribed.

Syntax#

C# Illustrative
Func<int, int, int> add = Add;        // two int inputs, int result
Action<string> log = Console.WriteLine; // string input, no result


public event EventHandler<OrderPlacedArgs>? OrderPlaced;
OrderPlaced?.Invoke(this, args);       // raise
service.OrderPlaced += HandleOrder;    // subscribe

Code example: Calculator operations and a kettle event#

C#Program.cs Illustrative
MathOp op = Add;
Console.WriteLine(op(6, 3));
op = Multiply;
Console.WriteLine(op(6, 3));
Console.WriteLine(Apply(10, 5, Subtract));


Func<string, string> shout = Shout;
Action<string> log = Console.WriteLine;
log(shout("saved"));


var kettle = new Kettle();
kettle.Boiled += SoundAlarm;
kettle.Boiled += WriteLog;
kettle.Heat();


static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static int Subtract(int a, int b) => a - b;
static int Apply(int a, int b, MathOp operation) => operation(a, b);
static string Shout(string text) => text.ToUpper() + "!";


static void SoundAlarm(object? sender, int temperature) =>
    Console.WriteLine(
quot;Alarm: water boiled at
{temperature} degrees");
static void WriteLog(object? sender, int temperature) => Console.WriteLine("Log: kettle switched off"); delegate int MathOp(int a, int b); class Kettle { public event EventHandler<int>? Boiled; public void Heat() { for (int t = 90; t <= 100; t += 5) { Console.WriteLine(
quot;Heating...
{t}");
} Boiled?.Invoke(this, 100); } }
Expected output
9
18
5
SAVED!
Heating... 90
Heating... 95
Heating... 100
Alarm: water boiled at 100 degrees
Log: kettle switched off

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. MathOp op = Add stores the Add method in a variable; op(6, 3) calls it.
  2. Assigning Multiply to the same variable changes what op does.
  3. Apply receives a method as a parameter and calls it — the caller chose Subtract.
  4. Func<string, string> and Action<string> are built-in delegate types, so no custom declaration is needed.
  5. Two methods subscribe to kettle.Boiled with +=. When Heat raises the event, both run in order.

Real-world use case#

Desktop and Blazor UIs use events for button clicks. In back-end code, an OrderService raises OrderPlaced and separate handlers send the email, update stock and record analytics — without OrderService knowing about any of them.

Common mistakes#

Raising an event with no subscribers

Boiled(this, 100) throws NullReferenceException when nobody subscribed. Use Boiled?.Invoke(this, 100).

Forgetting to unsubscribe

A long-lived publisher keeps short-lived subscribers alive in memory. Unsubscribe with -= when the subscriber is disposed.

Public delegate fields instead of events

With a plain public delegate, outside code can overwrite it with = and remove every other subscriber. The event keyword prevents that.

Best practices#

  • Use Func and Action instead of custom delegate types
  • Expose events, not public delegate fields
  • Raise events with ?.Invoke
  • Use EventHandler<T> for the standard (sender, args) shape

Remember this#

Delegate = a variable that holds a method.

Func returns a value (last type = result). Action returns nothing.

Event: += to subscribe, -= to unsubscribe, ?.Invoke to raise.

Practice exercise#

Your task: Write a method Transform(List<string> items, Func<string, string> change) that returns a new list with change applied to every item. Call it with a method that adds "#" in front of each word.

Need a hint?
Loop through items, call change(item) and add the result to a new list.
Show solution
C# Illustrative
var tags = Transform(["csharp", "dotnet"], AddHash);
Console.WriteLine(string.Join(" ", tags));   // #csharp #dotnet


static List<string> Transform(List<string> items, Func<string, string> change)
{
    var result = new List<string>();
    foreach (var item in items) result.Add(change(item));
    return result;
}


static string AddHash(string word) => "#" + word;

Mini quiz#

Delegates and Events quizMultiple choice Question 1 of 3

Which built-in delegate fits a method with signature bool IsValid(string s)?

Score so far: 0/0

Want more? Practise all C# MCQs · C# interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.