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.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 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.
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.
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.
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.
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.
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.
🔎 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.
Convention: name events with a verb describing what happened — Clicked , OrderPlaced , Finished — and name handler methods On<Event> like OnClicked .
Pro Tips
- 💡 Prefer Action and Func over 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 a NullReferenceException .
- 💡 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 , so MyEvent(this, e) crashes. Always raise it as MyEvent?.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 two int s and returning an int — 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
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.
🎉 Lesson Complete
- ✅ A delegate is a type that stores a method — you can pass it around like data
- ✅ Action returns void , Func returns a value, Predicate < T > returns bool
- ✅ 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 the EventHandler pattern
- ✅ The publish/subscribe pattern decouples a publisher from its listeners
- ✅ Next lesson: File I/O — reading and writing files with System.IO
Practice quiz
What is a delegate in C#?
- A loop construct
- A kind of collection
- A type that holds a reference to a method
- A keyword for inheritance
Answer: A type that holds a reference to a method. A delegate is a type that stores a reference to a method, so you can pass a method around like data.
What must a method have to be assignable to a particular delegate?
- A matching signature — the same parameter types and return type
- The same name as the delegate
- The static keyword
- A public access modifier
Answer: A matching signature — the same parameter types and return type. A method can be stored in a delegate only if its signature (parameter types and return type) matches the delegate's.
Which built-in delegate is for a method that returns nothing (void)?
- Func<T>
- Predicate<T>
- EventHandler
- Action<T>
Answer: Action<T>. Action<T> represents a method that returns void. Func returns a value; Predicate returns bool.
In Func<int, int, int>, which type is the return type?
- The first type parameter
- The last type parameter
- It has no return type
- All of them combined
Answer: The last type parameter. In Func<...>, the last type parameter is always the return type, so Func<int,int,int> means 'two ints in, an int out'.
What does Predicate<T> always return?
- bool
- void
- int
- string
Answer: bool. Predicate<T> is a special Func that always returns bool — a yes/no test on one value.
What is a lambda such as (a, b) => a + b?
- A delegate type declaration
- A new class definition
- A concise way to write a method inline, which C# converts into a delegate instance
- A loop
Answer: A concise way to write a method inline, which C# converts into a delegate instance. A lambda is a compact way to write a method inline; it isn't a delegate itself, but C# converts it into one to match the target type.
What is the difference between a delegate field and an event?
- An event runs faster
- Outside the declaring class, an event can only be subscribed to (+=) or unsubscribed from (-=) — not assigned with = or invoked
- An event cannot have subscribers
- There is no difference
Answer: Outside the declaring class, an event can only be subscribed to (+=) or unsubscribed from (-=) — not assigned with = or invoked. An event is a delegate with access restrictions: outside code can only += / -=, while only the declaring class can raise it or use =.
Which is the standard generic delegate for events, passing a sender and event data?
- Action<T>
- Func<T>
- Predicate<T>
- EventHandler<T>
Answer: EventHandler<T>. EventHandler<T> is the standard event delegate; it passes (object sender, T e) where e is an EventArgs subclass.
How should you raise an event safely when it may have no subscribers?
- MyEvent(this, e);
- MyEvent?.Invoke(this, e);
- MyEvent.Raise();
- new MyEvent();
Answer: MyEvent?.Invoke(this, e);. An event with no subscribers is null, so raising it directly throws. The null-conditional ?.Invoke skips the call when nobody is listening.
Why can a forgotten -= on a long-lived publisher cause a memory leak?
- Events use too much CPU
- Delegates are never collected
- The publisher still references the subscriber, so it can't be garbage-collected
- It corrupts the heap
Answer: The publisher still references the subscriber, so it can't be garbage-collected. If a subscriber never unsubscribes, the publisher keeps a reference to it, preventing garbage collection — a classic leak. Unsubscribe with -= (same handler reference).
Continue this course
- Previous: Async & Await
- Next: File I/O