A delegate is a variable that holds a method. An event is a delegate with restrictions that make it safe to expose publicly. Together they are how one part of a C# program tells another that something happened, without the two knowing about each other. This guide covers both, plus the memory leak that events cause if you never unsubscribe.
Delegates#
public delegate int Operation(int a, int b);
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
Operation op = Add;
Console.WriteLine(op(3, 4)); // 7
op = Multiply;
Console.WriteLine(op(3, 4)); // 12
The declaration defines a shape — two integers in, one integer out. Any method matching that shape can be assigned. This is what makes it possible to pass behaviour around as data.
Delegates are multicast: several methods can be attached, and all run in turn.
Action<string> log = Console.WriteLine;
log += message => File.AppendAllText("app.log", message + "\n");
log("Both of these run");
Action, Func and Predicate#
You rarely need to declare your own delegate type now. Three built-in ones cover almost everything:
Action greet = () => Console.WriteLine("hi"); // no parameters, no return
Action<string> log = m => Console.WriteLine(m); // one parameter, no return
Action<string, int> two = (m, n) => { }; // up to 16 parameters
Func<int> random = () => 4; // returns int
Func<int, int> square = n => n * n; // int in, int out
Func<int, int, int> add = (a, b) => a + b; // last type is the return
Predicate<int> isEven = n => n % 2 == 0; // same as Func<int, bool>
In Func, the last type parameter is the return type. That trips people up the first time.
These are what LINQ is built from:
numbers.Where(n => n > 10); // takes a Func<int, bool>
numbers.Select(n => n * 2); // takes a Func<int, int>
list.ForEach(Console.WriteLine); // takes an Action<T>
Events#
public class Downloader
{
public event EventHandler<ProgressEventArgs>? Progress;
public event EventHandler? Completed;
public void Run()
{
for (int i = 0; i <= 100; i += 10)
{
Progress?.Invoke(this, new ProgressEventArgs(i));
}
Completed?.Invoke(this, EventArgs.Empty);
}
}
public class ProgressEventArgs : EventArgs
{
public int Percent { get; }
public ProgressEventArgs(int percent) => Percent = percent;
}
var downloader = new Downloader();
downloader.Progress += (sender, e) => Console.WriteLine($"{e.Percent}%");
downloader.Completed += (sender, e) => Console.WriteLine("Done");
downloader.Run();
The convention is a sender and an arguments object derived from EventArgs. Following it means your events work with tooling and look familiar to other C# developers.
What the event keyword adds#
A public delegate field would let outside code do two things it should not:
// If Progress were a plain field:
downloader.Progress = null; // wipe out everyone else's handlers
downloader.Progress.Invoke(this, args); // raise it from outside
The event keyword makes both illegal. From outside the class, only += and -= are allowed. Raising it stays private to the declaring class. That is the whole difference, and it is worth having.
The null-conditional call#
Progress?.Invoke(this, args);
An event with no subscribers is null, and calling it throws. The question mark handles that — and it also handles a subtler race, where the last subscriber unsubscribes between your null check and your call. The null-conditional operator reads the delegate once, so this cannot happen.
// The old, thread-safe long form
var handler = Progress;
if (handler != null) handler(this, args);
The memory leak#
This is the most important thing on the page. Subscribing to an event creates a reference from the publisher to the subscriber:
public class Window
{
private readonly Downloader _downloader;
public Window(Downloader downloader)
{
_downloader = downloader;
_downloader.Progress += OnProgress; // downloader now holds a reference to this
}
private void OnProgress(object? sender, ProgressEventArgs e) { }
}
Close the window and it cannot be collected, because the long-lived downloader still points at it. Open and close it a thousand times and you have a thousand windows in memory, all still receiving events.
public class Window : IDisposable
{
public void Dispose()
{
_downloader.Progress -= OnProgress;
}
}
EventHandler<ProgressEventArgs> handler = (s, e) => Console.WriteLine(e.Percent);
downloader.Progress += handler;
downloader.Progress -= handler; // works, because we kept the reference
Handling exceptions in handlers#
protected virtual void OnProgress(ProgressEventArgs e)
{
foreach (var handler in
Progress?.GetInvocationList().Cast<EventHandler<ProgressEventArgs>>()
?? Enumerable.Empty<EventHandler<ProgressEventArgs>>())
{
try
{
handler(this, e);
}
catch (Exception ex)
{
_logger.LogError(ex, "A progress handler failed");
}
}
}
By default, if the second of five handlers throws, the remaining three never run. Iterating the invocation list yourself isolates them. Worth doing when subscribers are outside your control; unnecessary within a small application.
Async handlers#
// Fire and forget - exceptions vanish
downloader.Completed += async (s, e) => await SaveAsync();
An async void handler cannot be awaited, so the publisher continues immediately and any exception is lost. It is unavoidable with classic events, so keep such handlers short and wrap their bodies in try/catch. For genuinely asynchronous notification, consider a channel or a message queue rather than an event.
Questions people ask#
What is the difference between a delegate and an event?
An event is a delegate with access restrictions. Outside code can subscribe and unsubscribe but cannot replace the list or raise it.
Should I use EventHandler or my own delegate type?
EventHandler and its generic form, for consistency with the rest of .NET. A custom type is only worth it when the sender-and-args shape genuinely does not fit.
Are events thread-safe?
Subscribing and unsubscribing are, because the compiler generates locked add and remove methods. Raising is not — use the null-conditional invoke, and be aware handlers may run on whichever thread raised the event.
What is IObservable used for?
It is the reactive alternative, with proper support for composing, filtering and unsubscribing via a disposable. Worth reaching for when you have streams of events rather than occasional notifications.
Where to go next#
- C# advanced language features — LINQ, which is delegates all the way down.
- C# design patterns — the observer pattern events implement.
- Asynchronous programming with C# — the async handler problem in context.