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.”
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
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#
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#
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);
}
}
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#
- MathOp op = Add stores the Add method in a variable; op(6, 3) calls it.
- Assigning Multiply to the same variable changes what op does.
- Apply receives a method as a parameter and calls it — the caller chose Subtract.
- Func<string, string> and Action<string> are built-in delegate types, so no custom declaration is needed.
- 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#
Boiled(this, 100) throws NullReferenceException when nobody subscribed. Use Boiled?.Invoke(this, 100).
A long-lived publisher keeps short-lived subscribers alive in memory. Unsubscribe with -= when the subscriber is disposed.
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?
Show solution
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#
Which built-in delegate fits a method with signature bool IsValid(string s)?
Want more? Practise all C# MCQs · C# interview questions
My notes#
Saved in this browser.