Menu

C# Events: event Keyword, EventHandler, Subscribe and Raise

How events work in C#: declaring an event with EventHandler and EventHandler<T>, custom EventArgs classes, subscribing and unsubscribing with += and -=, raising an event safely with ?.Invoke, why an event beats a public delegate field, and the memory leak caused by a forgotten unsubscribe.

This page includes runnable editors - edit, run, and see output instantly.

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 with EventArgs.Empty.
  • EventHandler<TEventArgs>: void (object sender, TEventArgs e), where TEventArgs is your class carrying the data.

The conventions around them:

  • sender is the object that raised the event, usually this.
  • The data class is named {Something}EventArgs, derives from EventArgs (optional since .NET 4.5, still customary), and exposes read-only properties.
  • The event is named for what happened: OrderPlaced, Closed, PriceChanged. A ...ing name (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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED