An event lets a class announce that something happened without knowing who is listening. Other objects subscribe with +=, and when the class raises the event, every subscriber's method runs.
Output:
Order A-17 saved
receipt emailed for A-17
Order A-18 saved
receipt emailed for A-18
free gift added to A-18
Shop does not know that receipts or gifts exist. It raises OrderPlaced, and whatever subscribed reacts. Adding a new reaction (loyalty points, analytics) means adding a subscriber, not editing PlaceOrder.
Declaring an Event
An event declaration is a delegate-typed member with the event keyword:
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
The delegate type fixes the handler signature. .NET code almost always uses one of two built-in types, and following the convention makes your events familiar to every other .NET developer:
EventHandler:void (object sender, EventArgs e), for events that carry no data. Raise it withEventArgs.Empty.EventHandler<TEventArgs>:void (object sender, TEventArgs e), whereTEventArgsis your class carrying the data.
The conventions around them:
senderis the object that raised the event, usuallythis.- The data class is named
{Something}EventArgs, derives fromEventArgs(optional since .NET 4.5, still customary), and exposes read-only properties. - The event is named for what happened:
OrderPlaced,Closed,PriceChanged. A...ingname (Closing) is used for events raised before the action, often with a way to cancel it. - Raising goes through a
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e)method in classes meant to be inherited, so derived classes can hook in.
Subscribing and Unsubscribing
+= adds a handler and -= removes it. A handler can be a method or a lambda. To remove a lambda later, keep it in a variable, because writing the same lambda again creates a different delegate that -= will not find:
Output:
set to 32
display shows 32 C
alarm: too hot
set to 35
display shows 35 C
set to 20
display shows 20 C
Removing a handler that was never added is not an error; it does nothing. EventHandler<int> shows that the type argument does not have to derive from EventArgs on modern .NET, although a dedicated class leaves room to add fields later without breaking subscribers.
Handlers run synchronously, in subscription order, on the thread that raised the event. If one handler throws, the remaining handlers do not run and the exception propagates to the code that raised the event.
Raising an Event Safely
An event with no subscribers holds null. Calling it directly would throw NullReferenceException, so raise it with the null-conditional operator:
OrderPlaced?.Invoke(this, args);
Before C# 6 the pattern was to copy the field into a local, check it, and invoke the copy. The copy matters in multithreaded code: checking OrderPlaced != null and then calling OrderPlaced(...) reads the field twice, and another thread could remove the last handler in between. ?. reads it once, so it is both shorter and correct.
Why event and Not a Public Delegate Field
Without the event keyword, a public delegate field works for subscribing, but it hands every subscriber full control:
Output:
analytics ping
analytics ping
One = typed instead of += silently removed the two earlier handlers, and outside code could raise the "click" without any click. Marking the field event makes both lines compile errors:
error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')
Inside Button, the event still behaves like a normal delegate field, so the class can invoke it and check it for null.
The Forgotten Unsubscribe Leak
When you subscribe, the event stores a delegate, and the delegate holds a reference to the subscriber object. As long as the publisher is alive and the handler is attached, the subscriber cannot be garbage collected, and it keeps receiving events after you are done with it:
Output:
closed widget shows 101.5
open widget shows 101.5
subscribers left: 1
closed widget shows 99.0
Setting leaky to null did nothing for the feed: its handler list still references that widget, so the "closed" widget keeps updating and stays in memory. The widget in the using block unsubscribed in Dispose and stopped receiving. This is one of the most common memory leaks in .NET desktop and server code, especially with static events and application-wide services, which live until the process ends.
The rule: whoever subscribes to an event on an object that outlives it must unsubscribe, usually in Dispose (see the using statement). When the publisher and subscriber have the same lifetime, such as a form and its own buttons, no unsubscribe is needed.
Custom add and remove Accessors
An event can define what += and -= do, like a property's get and set. This is rare, but it is how frameworks store handlers in a shared dictionary or forward them to another object:
private EventHandler closed;
public event EventHandler Closed
{
add { Console.WriteLine("subscriber added"); closed += value; }
remove { Console.WriteLine("subscriber removed"); closed -= value; }
}
With custom accessors, the class raises the event through the backing field (closed?.Invoke(this, EventArgs.Empty)), since the event itself no longer has storage.
Frequently Asked Questions
What is an event in C#?
An event is a class member that lets other objects ask to be notified when something happens. It is backed by a multicast delegate, but outside code can only add handlers (+=) and remove them (-=). Only the class that declares the event can raise it.
What is the difference between an event and a delegate in C#?
A delegate is a type that references methods. An event is a member declared with a delegate type plus the event keyword, which restricts access: from outside the class you cannot invoke it, read its handler list or assign it with =, only subscribe and unsubscribe. A public delegate field allows all of that, so any subscriber could wipe out the others or fire the event.
How do I pass data with a C# event?
Derive a class from EventArgs with the data as read-only properties (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) and declare the event as EventHandler<OrderPlacedEventArgs>. Raise it with OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.
How do I raise an event safely in C#?
Use MyEvent?.Invoke(this, args);. An event with no subscribers is null, so calling it directly throws NullReferenceException. The ?. operator reads the field once, which also avoids a race where another thread removes the last handler between a null check and the call.
Can C# events cause memory leaks?
Yes. Subscribing stores a delegate that references the subscriber, so the publisher keeps the subscriber alive for as long as the publisher lives. A long-lived publisher (a static event, an app-wide service) with short-lived subscribers that never unsubscribe keeps every one of them in memory. Unsubscribe with -= when the subscriber is done, typically in Dispose.