Delegates and Events
A delegate is a type whose instances point at a method with a specific signature.
🎯 What you'll learn
- Delegates: Type-Safe Function References
- Built-in Generic Delegates
- Lambdas and Closures
- Multicast Delegates
- Events: Delegates With Access Control
1Delegates: Type-Safe Function References
A delegate is a type whose instances point at a method with a specific signature.
delegate int BinaryOp(int a, int b);
static int Add(int a, int b) => a + b;
BinaryOp op = Add; // method group conversion
int r = op(2, 3); // 5 — same as op.Invoke(2, 3)Delegates are objects: you can store them in fields, pass them as arguments and return them.
2Built-in Generic Delegates
You rarely declare custom delegate types today:
| Type | Signature | Example |
|---|---|---|
Action / Action<T1..T16> | returns void | Action<string> log = Console.WriteLine; |
Func<T1..T16, TResult> | returns TResult (last type arg) | Func<int, int, int> add = (a, b) => a + b; |
Predicate<T> | returns bool | Predicate<int> isEven = n => n % 2 == 0; |
Custom delegates still make sense when the name documents intent (EventHandler<TEventArgs>, RequestDelegate in ASP.NET Core) or when you need ref/out parameters.
3Lambdas and Closures
Func<int, Func<int, int>> makeAdder = x => y => x + y;
var add5 = makeAdder(5);
add5(10); // 15
int counter = 0;
Action inc = () => counter++; // captures the VARIABLE, not its value
inc(); inc();
Console.WriteLine(counter); // 24Multicast Delegates
Every delegate can hold an invocation list. += appends, -= removes the last matching entry.
Action notify = () => Console.WriteLine("email");
notify += () => Console.WriteLine("sms");
notify(); // email, sms (in subscription order)For a non-void multicast delegate, invoking it returns only the last handler's result. And if one handler throws, the remaining handlers don't run.
5Events: Delegates With Access Control
public class OrderService
{
public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;
public void Place(Order order)
{
// ... save ...
OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(order.Id));
}
}
public record OrderPlacedEventArgs(int OrderId); // EventArgs subclass is optional in modern .NET
var svc = new OrderService();
svc.OrderPlaced += (sender, e) => Console.WriteLine($"placed {e.OrderId}");The event keyword restricts what outside code can do:
| Operation | public delegate field | public event |
|---|---|---|
+= / -= subscribe | ✅ | ✅ |
= handler (replace everyone) | ✅ | ❌ compile error |
svc.OrderPlaced(...) (raise) | ✅ | ❌ only the declaring class |
= null (wipe subscribers) | ✅ | ❌ |
6The Standard Event Pattern
- Signature
void Handler(object? sender, TEventArgs e)— useEventHandler/EventHandler<T>. - Raise through a
protected virtual void OnXxx(TEventArgs e)method so subclasses can hook in. - Copy-then-invoke (
OrderPlaced?.Invoke(...)) is thread-safe against a concurrent unsubscribe because delegates are immutable —?.reads the field once.
Under the HoodWhat a Delegate and an Event Compile To▾
A delegate type compiles to a sealed class deriving from System.MulticastDelegate, with a runtime-implemented Invoke method. Each instance stores:
_target— the object to call the method on (nullfor static methods; for a capturing lambda, an instance of a compiler-generated closure class that holds the captured variables as fields)_methodPtr— the code address of the method_invocationList— an array of delegates when there's more than one subscriber
+= calls Delegate.Combine, which allocates a new delegate with a new array — delegates never mutate in place. That's why a copy you already read stays stable while other threads subscribe or unsubscribe.
A field-like event compiles to a private delegate field plus add_X/remove_X accessor methods that perform Combine/Remove in a lock-free Interlocked.CompareExchange loop. Outside the class, x.OrderPlaced += h becomes x.add_OrderPlaced(h) — there is no accessor for raise or assign, which is how the compiler enforces the table above.
⚠️ Common Pitfalls
- Memory leaks via events. The publisher's invocation list holds a strong reference to every subscriber's
_target. A long-lived publisher (static event, singleton) keeps short-lived subscribers alive forever unless they-=. - Unsubscribing a lambda.
x.E -= () => Foo();creates a new delegate that never matches. Store the handler in a variable to remove it. - Raising without a null check — an event with no subscribers is
null. Use?.Invoke. - Closures over loop variables in old
forloops capture one shared variable. (foreachgets a fresh variable per iteration since C# 5;fordoes not.) - Relying on return values or exception flow of multicast delegates — only the last return value survives and one throwing handler stops the rest. Iterate
GetInvocationList()when every handler must run.
📋 Quick Reference
| Need | Use |
|---|---|
| callback returning nothing | Action<T> |
| callback returning a value | Func<T, TResult> |
| condition | Predicate<T> / Func<T,bool> |
| publish notifications | public event EventHandler<TArgs>? Xxx; |
| raise safely | Xxx?.Invoke(this, args); |
| run every handler even if one throws | foreach (var d in Xxx.GetInvocationList()) |
🎤 Interview Q&A
Q1What is a delegate?core▾
A type-safe function pointer: Func<T, TResult>, Action<T>, Predicate<T> or custom delegate types. Delegates can point to methods or lambdas and can be multicast (several methods).
- Lambdas and closures? Lambdas capture variables, allocating closures.
Q2What are events and how do they differ from delegates?core▾
An event is a delegate member with restricted access: outside code can only subscribe (+=) or unsubscribe (-=), while only the declaring class can raise it. The standard pattern uses EventHandler<TEventArgs>.
- Why restrict? Prevents outsiders from invoking or clearing subscribers.
Q3How can events cause memory leaks?core▾
The publisher holds references to subscribers; if a long-lived publisher keeps short-lived subscribers registered, they're never collected. Unsubscribe in Dispose or use weak event patterns.
- Common in? UI apps and static events.
Q4How do you raise events safely?senior▾
Use Handler?.Invoke(this, args) to avoid a race between null check and invocation, and consider exceptions in one subscriber stopping others in multicast invocation.
- Async event handlers?
async voidhandlers can't be awaited; consider async callbacks or channels.
Q5When would you use events vs other patterns?core▾
Events suit in-process notifications (UI, domain events within a process). For decoupled processing, use MediatR notifications, Channels, or message brokers across services.
- Observer pattern? Events implement it; IObservable/Rx for streams.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.