Events
Intro
An event is a restricted delegate member that implements publisher-subscriber communication. Outside the declaring type, consumers can only subscribe (+=) and unsubscribe (-=); they cannot invoke or replace the delegate invocation list. This encapsulation is why events are preferred over raw public delegates in APIs.
The standard .NET event signature uses EventHandler or EventHandler<TEventArgs>.
public sealed class PriceFeed
{
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
private decimal _price;
public void UpdatePrice(decimal newPrice)
{
if (newPrice == _price) return;
_price = newPrice;
OnPriceChanged(new PriceChangedEventArgs(newPrice));
}
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
=> PriceChanged?.Invoke(this, e);
}
public sealed class PriceChangedEventArgs : EventArgs
{
public decimal Price { get; }
public PriceChangedEventArgs(decimal price) => Price = price;
}
Why event Instead of Public Delegate Field
With a public delegate field, any caller can do dangerous operations like:
- assign
publisher.Callback = null - invoke
publisher.Callback(...) - replace all handlers
event blocks these operations for external code and exposes only subscription semantics.
Custom add and remove
You can define explicit accessors for advanced scenarios (thread-safe collections, weak subscriptions, deduplication):
private EventHandler? _tick;
private readonly object _gate = new();
public event EventHandler Tick
{
add
{
lock (_gate)
_tick += value;
}
remove
{
lock (_gate)
_tick -= value;
}
}
Pitfalls
- Memory leaks via long-lived publishers: subscribers stay alive while subscribed.
- Forgotten unsubscribe: common in UI/view-model/service lifetimes.
Example leak-safe subscription pattern:
public sealed class Listener : IDisposable
{
private readonly PriceFeed _feed;
public Listener(PriceFeed feed)
{
_feed = feed;
_feed.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object? sender, PriceChangedEventArgs e)
=> Console.WriteLine(e.Price);
public void Dispose()
=> _feed.PriceChanged -= OnPriceChanged;
}
Tradeoffs
- Events vs public delegate fields: A public delegate field lets any external caller replace, null out, or directly invoke the handler. The
eventkeyword restricts external callers to+=/-=only, preserving publisher control. Always useeventin public APIs. - Events vs
IObservable<T>(Rx): Events are synchronous, single-publisher, multicast notifications with no composition support.IObservable<T>from Reactive Extensions supports filtering, merging, debouncing, retrying, and async continuations — at the cost of a dependency and a steeper learning curve. UseIObservable<T>when you need stream operators; events for simple point-to-point notifications. - Custom
add/removeoverhead: The default event implementation stores handlers in a multicast delegate (immutable; every+=/-=allocates a new list). In high-frequency subscribe/unsubscribe scenarios, custom accessors backed by aConcurrentDictionaryor locked collection reduce per-operation allocation.
Questions
An event exposes only add/remove from outside the declaring type. A delegate field can be invoked, replaced, or nulled by external callers. Events preserve publisher ownership of invocation.
PriceChanged?.Invoke(this, e) preferred over if (PriceChanged != null) PriceChanged(this, e)?The null-conditional form avoids the race where another thread unsubscribes between check and call. It uses a copied delegate reference for the invocation expression.
The publisher keeps strong references to subscriber handlers. If the publisher outlives subscribers, those subscribers cannot be garbage-collected. Prevent with explicit unsubscribe (Dispose), weak-event pattern, or scoped subscription helpers.
EventArgs inheritance mandatory in modern .NET?
No. In modern .NET, EventHandler<T> no longer requires T : EventArgs. You can use any payload type, but EventArgs-based design remains the most idiomatic and interoperable style.
Copy the invocation list using GetInvocationList() and invoke handlers individually in try/catch. Direct event invocation stops at first exception.
Links
- Standard .NET event patterns — official guide to
EventHandler<T>,EventArgs, and the raise/subscribe pattern. - Events - .NET guide — conceptual overview of the event model, delegates, and multicast invocation.
- Modern events in C# — covers relaxed
EventArgsconstraint and modern subscription patterns. - Null-conditional operator and thread-safe delegate invoke — explains why
?.Invokeis safer than null-check + call. - Weak event patterns (WPF) — pattern for preventing memory leaks when subscriber lifetime is shorter than publisher lifetime.