Events
What the event keyword adds to a delegate, raising safely with ?.Invoke, unsubscribing correctly, and what a throwing handler does to the rest.
After this lesson you can
- Declare an event with EventHandler<T> and raise it safely
- Explain why -= with a new lambda removes nothing
- Predict what happens to later handlers when one handler throws
An event is a multicast delegate field with a guard around it.
public class Button
{
public event EventHandler? Clicked; // the event
public void Click() => Clicked?.Invoke(this, EventArgs.Empty);
}
Outside the declaring class, code may only += (subscribe) and -=
(unsubscribe). It cannot invoke the event, assign it with =, or read
its invocation list. Those restrictions are the whole point: with a
plain public delegate field, any subscriber could fire the event or
wipe out everyone else's handlers with a single =.
The .NET convention is EventHandler or EventHandler<TEventArgs>:
the handler receives the sender and an object carrying the details.
Clicked?.Invoke(...) matters because an event with no subscribers is
null; calling it directly would throw NullReferenceException.
Unsubscribing
-= removes a delegate equal to the one given. Two delegates are
equal when they point to the same method on the same target. Writing
the same lambda text twice produces two different delegates:
public class Button{ public event EventHandler? Clicked; public int HandlerCount => Clicked?.GetInvocationList().Length ?? 0; public void Click() => Clicked?.Invoke(this, EventArgs.Empty);} public static class Solution{ static readonly List<string> Log = new(); static void OnClick(object? sender, EventArgs e) => Log.Add("method"); public static string Describe() { Log.Clear(); var b = new Button(); b.Clicked += (s, e) => Log.Add("lambda"); b.Clicked += OnClick; int before = b.HandlerCount; b.Clicked -= (s, e) => Log.Add("lambda"); // a different delegate instance: removes nothing int afterLambda = b.HandlerCount; b.Clicked -= OnClick; // equal delegate (same method, same target): removed int afterMethod = b.HandlerCount; b.Click(); return $"before={before} after -= lambda={afterLambda} after -= method={afterMethod} ran={string.Join(",", Log)}"; }}The lambda handler survived -= (the count stayed at 2 and it still
ran), while the method-group handler was removed. To unsubscribe a
lambda, keep it in a variable and use that variable for both += and
-=.
When a handler throws
Raising an event calls the handlers one after another on the raising
thread. An exception from one handler propagates out of Invoke, and
the handlers after it never run:
public class Sensor{ public event Action<int>? Reading; public void Publish(int value) => Reading?.Invoke(value);} public static class Solution{ public static string Describe() { var log = new List<string>(); var s = new Sensor(); s.Publish(1); // no subscribers: ?.Invoke does nothing s.Reading += v => log.Add("first:" + v); s.Reading += v => throw new InvalidOperationException("broken handler"); s.Reading += v => log.Add("third:" + v); try { s.Publish(2); } catch (InvalidOperationException e) { log.Add("caught:" + e.Message); } return string.Join(", ", log); }}The first Publish(1), with no subscribers, did nothing thanks to
?.. For Publish(2), the first handler ran, the second threw, and
the third was skipped. If every handler must run, walk
GetInvocationList() and wrap each call in its own try/catch.
Common mistakes
- Raising without a null check.
Clicked(this, e)throwsNullReferenceExceptionwhen nobody subscribed. Use?.Invoke. - Unsubscribing with a fresh lambda. Shown above: nothing is removed. Store the delegate.
- Forgetting to unsubscribe from a long-lived publisher. The publisher's invocation list holds a reference to each subscriber, so a short-lived object subscribed to a static or singleton event is never garbage-collected.
- Assuming a throwing handler only affects itself. It stops the remaining handlers and surfaces in the code that raised the event.
What interviewers ask
- "What is the difference between an event and a public delegate
field?" Outside the class an event allows only
+=and-=; a field can also be invoked or overwritten by anyone. - "How can events cause memory leaks?" The publisher references every subscriber through its handlers; if the publisher outlives them and they never unsubscribe, they stay reachable.
Exercise · Pro
Raise an event only when the temperature crosses the limit
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade