Lesson 13 • Expert Track
Delegates & Events
By the end of this lesson you'll be able to treat a method like data — store it in a variable, pass it to another method, and chain several together. Then you'll use that power to build events: the publish/subscribe pattern that drives buttons, timers, and notifications across real C# apps.
What You'll Learn
- Declare and use a custom delegate type to store and pass methods
- Use the built-in Action, Func, and Predicate delegates with lambdas
- Chain multiple methods with multicast delegates (+= and -=)
- Declare an event and raise it safely with ?.Invoke(...)
- Subscribe and unsubscribe handlers with += and -=
- Apply the EventHandler/EventArgs publish-subscribe pattern
new.💡 Real-World Analogy
A delegate is like writing down someone's phone number. The number isn't the call itself — it's a way to reach a person later. You can hand that number to a friend, store it for tomorrow, or save several numbers and ring them all at once (a multicast delegate). An event is like a subscription list for a newsletter: anyone can sign up (+=) or cancel (-=), and when the publisher sends an issue, everyone on the list gets notified — but the publisher never needs to know who's reading. You'll build exactly that pattern in this lesson.
Running C# Locally: Install the .NET SDK or use dotnetfiddle.net to run every example below.
📊 The Building Blocks at a Glance
| Tool | What it is | Example | When to use |
|---|---|---|---|
| delegate | A custom method-reference type | delegate int Op(int a, int b); | A reusable, named callback shape |
| Action<T> | Built-in, returns void | Action<string> log; | A callback that does something |
| Func<T,R> | Built-in, returns a value | Func<int,int,int> add; | A callback that computes a result |
| Predicate<T> | Built-in, returns bool | Predicate<int> isEven; | A yes/no test on one value |
| event | A guarded subscription list | public event EventHandler Click; | Notify many listeners (pub/sub) |
Rule of thumb: reach for Action/Func/Predicate first — you'll rarely need to declare your own delegate. Use an event when you want to broadcast a notification.
1. Delegates — Methods as Values
A delegate is a type that can hold a reference to a method, as long as that method has a matching signature (the same parameter types and return type). Once a method is in a delegate variable, you can store it, re-point it, or pass it to another method — exactly like passing a number or a string. That last trick, passing a method as an argument, is what makes delegates so powerful: a method can take behaviour from its caller. Read this worked example, run it, then you'll write your own.
Worked example: a custom delegate
Read every comment, run it, and notice the same variable can call different methods.
using System;
// A DELEGATE is a type that holds a reference to a method.
// Think of it as a variable that stores a method instead of a value.
// This one can point at ANY method that takes (int, int) and returns int.
delegate int MathOperation(int a, int b);
class Program
{
// Three methods that all MATCH the delegate's signature.
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 void Mai
...Your turn. The program below uses Func<int, int, int> — the built-in delegate for "two ints in, one int out". Fill in the two ___ blanks to assign a lambda and invoke it, then run it.
🎯 Your turn: complete a Func delegate
Assign an adding lambda and invoke it; the sum should be 12.
using System;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — finish the two blanks, then press "Try it Yourself".
// Func<int, int, int> means: takes two ints, RETURNS an int.
// 1) Assign a lambda that ADDS its two inputs.
Func<int, int, int> add = (a, b) => ___; // 👉 a + b
// 2) Invoke the delegate like a normal method call.
int sum = ___; // 👉 add(7, 5)
Console.WriteLine($"7 + 5 = {sum}
...2. Action, Func & Predicate
You almost never need to declare a custom delegate anymore, because C# ships three generic ones that cover nearly every case. Action is for methods that return nothing (void). Func is for methods that return a value — the last type parameter is always the return type, so Func<int, int, int> means "two ints in, an int out". Predicate is a special Func that always returns bool — a yes/no test. Combined with lambdas (the => shorthand for a tiny inline method), they let you pass behaviour around with almost no ceremony.
Worked example: Action, Func & Predicate
See all three built-in delegates, plus passing a Func to make a method reusable.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Action<T> — a delegate for a method that RETURNS NOTHING (void).
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice"); // Hello, Alice!
greet("Bob"); // Hello, Bob!
// Func<T..., TResult> — RETURNS a value; the LAST type is the result.
Func<int, int, int> add = (a, b) => a + b
...3. Multicast Delegates
A single delegate variable can hold several methods at once — that's a multicast delegate. You add a method to the chain with += and remove one with -=. When you invoke the delegate, every method runs in turn, in the order they were added. This is the exact mechanism that events are built on, so understanding it here makes the next section click instantly.
Worked example: one delegate, many methods
Watch += chain methods together and -= remove one again.
using System;
class Program
{
static void Main()
{
// A delegate can point at MORE THAN ONE method at once — a
// "multicast" delegate. Use += to add methods to the chain.
Action<string> log = Console.WriteLine; // method #1
log += msg => Console.WriteLine($"[FILE] {msg}"); // method #2 added
// ONE call runs BOTH methods, in the order they were added.
log("Saving file...");
// Saving file...
// [FILE] Saving file..
...4. Events — the Publish/Subscribe Pattern
An event is a multicast delegate with guard rails. It lets a class — the publisher — announce that something happened, while any number of subscribers react however they want. The key difference from a plain delegate is encapsulation: outside code can only += (subscribe) or -= (unsubscribe); only the publishing class can actually raise the event. The standard shape uses the EventHandler<T> delegate, which always passes a sender (who raised it) and an EventArgs object (the details). Read the worked example, then you'll wire up your own event.
Worked example: an order-processing event
One PlaceOrder call notifies every subscriber — the publisher never knows who they are.
using System;
// Custom EventArgs carries extra data to the subscribers.
// By convention an EventArgs subclass ends in "EventArgs".
class OrderEventArgs : EventArgs
{
public string OrderId { get; }
public decimal Total { get; }
public OrderEventArgs(string orderId, decimal total)
{
OrderId = orderId;
Total = total;
}
}
// The PUBLISHER — it announces events but knows nothing about who listens.
class OrderProcessor
{
// EventHandler<T> is the standard g
...Now you try. The Button class below needs you to (1) raise its Clicked event safely and (2) subscribe a handler to it. Fill in the two blanks marked ___, then run it.
🎯 Your turn: declare, subscribe & raise an event
Raise the event with ?.Invoke and subscribe a handler with +=.
using System;
class Button
{
// An EVENT is a notification other code can subscribe to.
// EventHandler is the standard delegate: (object sender, EventArgs e).
public event EventHandler Clicked;
public void Press()
{
Console.WriteLine("Button pressed...");
// 🎯 YOUR TURN #1 — RAISE the event safely (skip if no subscribers).
Clicked?.___(this, EventArgs.Empty); // 👉 Invoke
}
}
class Program
{
static void Main()
{
var button =
...🔎 Deep Dive: why an event and not just a public delegate?
A plain public delegate field would let any outside code overwrite it with =, wiping out everyone else's subscriptions, or even invoke it whenever they felt like it. The event keyword closes both holes: from outside the class you can only add (+=) or remove (-=) handlers — never assign with = and never raise it. That guarantee is the whole point of events.
public event EventHandler Clicked; // safe: outside code can only += / -= // inside the class — only the owner may raise it: Clicked?.Invoke(this, EventArgs.Empty); // outside the class: btn.Clicked += OnClick; // ✅ allowed btn.Clicked = OnClick; // ❌ compile error — can't assign to an event btn.Clicked.Invoke(...); // ❌ compile error — can't raise from outside
Convention: name events with a verb describing what happened — Clicked, OrderPlaced, Finished — and name handler methods On<Event> like OnClicked.
Pro Tips
- 💡 Prefer
ActionandFuncover custom delegates: they're built in, generic, and instantly familiar to every C# developer — less boilerplate to read and write. - 💡 Always raise events with
?.Invoke(...): the null-conditional operator skips the call when there are no subscribers, avoiding aNullReferenceException. - 💡 Use
EventHandler<T>for events rather than a raw delegate — the(sender, e)convention is what every C# tool and developer expects. - 💡 Lambdas (
=>) are the quickest way to create a delegate instance inline — but if you'll need to unsubscribe later, give the handler a named method so you can pass the same reference to-=. - 💡 Always unsubscribe (
-=) long-lived subscriptions: a subscriber the publisher still references can't be garbage-collected — a classic memory leak.
Common Errors (and the fix)
- "System.NullReferenceException" when raising an event: an event with zero subscribers is
null, soMyEvent(this, e)crashes. Always raise it asMyEvent?.Invoke(this, e);— the?.simply does nothing if no one is listening. - "CS0070: The event 'X' can only appear on the left hand side of += or -=": you tried to invoke or assign an event from outside the class. Only the declaring class can raise it; outside code may just subscribe (
+=) or unsubscribe (-=). - Using
=instead of+=on an event/delegate:btn.Clicked = handler;would replace every existing subscriber (and won't even compile for an event). Use+=to add a handler without clobbering the others. - "CS0123: No overload for 'Method' matches delegate 'D'": the method's signature doesn't match the delegate. A
Func<int, int, int>needs a method taking twoints and returning anint— check the parameter types and the return type line up exactly. - Memory leak from a forgotten
-=: if a short-lived object subscribes to an event on a long-lived publisher and never unsubscribes, the publisher keeps it alive forever. Unsubscribe with-=when the subscriber is done (pass the same handler reference you subscribed with).
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Declare a delegate type | delegate int Op(int a, int b); | Custom callback shape |
| Point at a method | Op op = Add; | No parentheses |
| Func with a lambda | Func<int,int,int> f = (a,b) => a+b; | Last type = result |
| Action (void) | Action<string> log = Console.WriteLine; | Returns nothing |
| Add to a chain | log += other; | Multicast |
| Declare an event | public event EventHandler Done; | On the publisher class |
| Subscribe / unsubscribe | obj.Done += H; obj.Done -= H; | From outside |
| Raise an event safely | Done?.Invoke(this, EventArgs.Empty); | Inside the class only |
Frequently Asked Questions
Q: What's the actual difference between a delegate and an event?
An event is a delegate underneath, but with access restrictions. Outside the declaring class you can only subscribe (+=) or unsubscribe (-=) — you can't assign it with = or invoke it. That protection is why you use events for the publish/subscribe pattern and plain delegates for simple callbacks.
Q: When do I use Action vs Func vs Predicate?
Use Action when the method returns nothing, Func when it returns a value (the last type parameter is the return type), and Predicate<T> as a tidy name for a Func<T, bool> — a yes/no test on one value.
Q: Why does raising my event throw a NullReferenceException?
An event with no subscribers is null. Raising it directly crashes. Always use the null-conditional operator: MyEvent?.Invoke(this, e); — it simply skips the call when nobody is listening.
Q: What's a lambda, and is it the same as a delegate?
A lambda — (a, b) => a + b — is a compact way to write a method inline. It isn't a delegate itself, but C# converts it into one to match an Action, Func, or custom delegate. It's just the most concise way to fill a delegate.
Mini-Challenge: an Event-Driven Stopwatch
No blanks this time — just a brief and an outline. Build a Stopwatch class that exposes a Finished event, ticks for a given number of seconds, then raises the event so a subscriber can react. Wire it up in Main with +=, then run it and check your output against the expected lines in the comments.
🎯 Mini-Challenge: build a Stopwatch event
Declare the event, raise it safely, subscribe a handler, and call Run(3).
using System;
// 🎯 MINI-CHALLENGE: an event-driven Stopwatch
// 1. Give Stopwatch an event: public event EventHandler Finished;
// 2. Add a method Run(int seconds) that prints "Tick" for each second,
// then RAISES Finished safely with Finished?.Invoke(this, EventArgs.Empty);
// 3. In Main: create a Stopwatch, SUBSCRIBE a handler with += that prints
// "Done! Time's up.", then call Run(3).
//
// ✅ Expected output:
// Tick
// Tick
// Tick
// Done! Time's up.
class Stopwatch
...🎉 Lesson Complete
- ✅ A delegate is a type that stores a method — you can pass it around like data
- ✅
Actionreturnsvoid,Funcreturns a value,Predicate<T>returnsbool - ✅ Lambdas (
=>) are the concise way to create a delegate instance - ✅ Multicast delegates chain methods with
+=and remove them with-= - ✅ An event is a guarded delegate — outside code can only
+=/-= - ✅ Raise events safely with
MyEvent?.Invoke(this, e)using theEventHandlerpattern - ✅ The publish/subscribe pattern decouples a publisher from its listeners
- ✅ Next lesson: File I/O — reading and writing files with System.IO
Sign up for free to track which lessons you've completed and get learning reminders.