Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 12 – Delegates, lambdas, events and functional style

Events, subscriptions and memory leaks

Declare and raise C# events with the standard EventHandler<T> pattern, remove handlers so that no screen leaks, and raise events safely when handlers fail.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: An event lets other classes subscribe, and only its owner raises it · A subscription keeps the subscriber alive until it is removed. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Declare and raise an event with the standard pattern of EventHandler<T>, a sender and event data
  • Remove a handler correctly, so that a long-lived publisher keeps no subscriber in memory
  • Choose how a subscription ends, with Dispose, an IDisposable token or a weak handler
  • Isolate failing handlers, and predict what a change during a raise or from many threads does

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare and raise an event with the standard pattern of EventHandler<T>, a sender and event data
  • Remove a handler correctly, so that a long-lived publisher keeps no subscriber in memory
  • Choose how a subscription ends, with Dispose, an IDisposable token or a weak handler
  • Isolate failing handlers, and predict what a change during a raise or from many threads does

Before you start

On this page

An inventory must tell two parts of a shop when tea falls below 5 units: the reorder service orders more and the dashboard shows a warning. The inventory should know neither, and a third listener should need no change to it. Events do this, under one hard rule: a subscription lasts until somebody removes it.

You will declare and raise events with the standard .NET pattern, end subscriptions before they leak memory, and see why removing a lambda so often removes nothing.

An event lets other classes subscribe, and only its owner raises it

An event: A member, declared with the event keyword, that holds handlers of a delegate type. Code outside its class can only add and remove handlers with += and -=; only the declaring class can raise it. is a delegate field with a guard around it: outside its class it may only appear on the left of += and -=, so other code can add and remove its own handlers but cannot replace everyone’s or raise the event. outside tries both:

outside/Program.cs C# · outside/Program.cs
// Outside the class that declares it, an event may only appear on the left of += and -=. These two lines try to
// replace every subscriber and to raise the event from outside.
var inventory = new Inventory();
inventory.LowStock += (sender, sku) => Console.WriteLine($"reorder {sku}");

inventory.LowStock = (sender, sku) => Console.WriteLine($"only me: {sku}");
inventory.LowStock(null, "TEA-250");

class Inventory
{
    public event EventHandler<string>? LowStock;
}
Building outside Python · outside_session.py
from session import run

# outside assigns to an event with = and calls it, both from code outside the class that declares it. dotnet run
# builds the project first and stops at the errors.
run("""
cd outside
!dotnet run
""")

Output

$ cd outside
$ dotnet run
outside/Program.cs(6,11): error CS0070: The event 'Inventory.LowStock' can only appear on the left hand side of += or -= (except when used from within the type 'Inventory') [outside/App.csproj]
outside/Program.cs(7,11): error CS0070: The event 'Inventory.LowStock' can only appear on the left hand side of += or -= (except when used from within the type 'Inventory') [outside/App.csproj]

The build failed. Fix the build errors and run again.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 outside_session.py

Both lines failed with CS0070. On a public delegate field both would compile: one = would drop every other subscriber, and any code could announce low stock that never happened.

.NET events follow one pattern, and inventory.cs uses all of it. The event’s type is EventHandler<TEventArgs>: its handlers take the object that raised it, the sender, and the event’s data, and return nothing. The data is a record, LowStockEventArgs, because TEventArgs no longer has to derive from EventArgs. One protected virtual method, OnLowStock, raises the event with LowStock?.Invoke(this, e), which reads the delegate once and calls it only if someone subscribed:

inventory.cs: a low-stock event with two subscribers C# · inventory/inventory.cs
// The standard .NET event pattern: EventHandler<TEventArgs>, a sender and the event's data, raised from one protected
// virtual method with ?.Invoke. The stock numbers are made up for this lesson.
var inventory = new Inventory(threshold: 5);
var reorder = new ReorderService();
var dashboard = new Dashboard();

inventory.LowStock += reorder.OnLowStock;
inventory.LowStock += dashboard.OnLowStock;

inventory.Receive("TEA-250", 12);
inventory.Take("TEA-250", 4);
inventory.Take("TEA-250", 5);

inventory.LowStock -= dashboard.OnLowStock;
inventory.Take("TEA-250", 1);
Console.WriteLine($"orders placed by the reorder service: {reorder.Orders}");

// The event's data. TEventArgs no longer has to derive from EventArgs; a positional record keeps it read-only.
record LowStockEventArgs(string Sku, int UnitsLeft, int Threshold);

class Inventory(int threshold)
{
    private readonly Dictionary<string, int> _units = [];

    public event EventHandler<LowStockEventArgs>? LowStock;

    public void Receive(string sku, int units) => _units[sku] = _units.GetValueOrDefault(sku) + units;

    public void Take(string sku, int units)
    {
        int left = _units[sku] -= units;
        Console.WriteLine($"took {units} of {sku}: {left} left");
        if (left < threshold)
        {
            OnLowStock(new LowStockEventArgs(sku, left, threshold));
        }
    }

    // The one place that raises the event. ?. reads LowStock once and calls it only when someone has subscribed.
    protected virtual void OnLowStock(LowStockEventArgs e) => LowStock?.Invoke(this, e);
}

class ReorderService
{
    public int Orders { get; private set; }

    public void OnLowStock(object? sender, LowStockEventArgs e)
    {
        Orders++;
        Console.WriteLine($"  reorder service: {e.Sku} has {e.UnitsLeft}, order {Orders} placed");
    }
}

class Dashboard
{
    public void OnLowStock(object? sender, LowStockEventArgs e) =>
        Console.WriteLine($"  dashboard: {e.Sku} is below {e.Threshold}, reported by the {sender?.GetType().Name}");
}

Output

took 4 of TEA-250: 8 left
took 5 of TEA-250: 3 left
  reorder service: TEA-250 has 3, order 1 placed
  dashboard: TEA-250 is below 5, reported by the Inventory
took 1 of TEA-250: 2 left
  reorder service: TEA-250 has 2, order 2 placed
orders placed by the reorder service: 2

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run inventory.cs

Taking 4 units left 8, and nothing was raised; taking 5 more left 3, below the threshold, and both handlers ran in the order they subscribed. After the dashboard’s -=, only the reorder service heard about the last unit: 2 orders in all. Step through the raise at 3 units:

Interactive · message by message One low-stock event, two subscribers Step through Take("TEA-250", 5). Watch the inventory raise LowStock once and each handler run in turn.
One low-stock event, two subscribers, step 5 of 5. Messages so far: Inventory to ReorderService: TEA-250, 3 left; Inventory to Dashboard: TEA-250, 3 left.Inventoryraises LowStockReorderServicesubscribed firstDashboardsubscribed secondTEA-250, 3 leftTEA-250, 3 left

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Stock falls below 5. Take("TEA-250", 5) leaves 3 units, below the threshold of 5, so the inventory calls its own OnLowStock with the event data: TEA-250, 3 left, threshold 5.
  2. One read of the event. OnLowStock runs LowStock?.Invoke(this, e). It reads the event's delegate once; two handlers are subscribed, so it calls them in the order they were added.
  3. The reorder service runs. The first handler, the reorder service's OnLowStock, gets the inventory as sender and the data, and places order 1.
  4. The dashboard runs. The second handler gets the same sender and the same data, and reports TEA-250 below 5, naming the Inventory as the sender.
  5. The raise is over. Both handlers have returned, and Take returns. The inventory never knew who listened: adding a third listener would need no change to it.

Microsoft’s guide to the pattern explains the void return: an event may have any number of listeners, none included, and they give nothing back. That suits notifications inside one application. IObservable<T> also reports when a stream ends or fails, IAsyncEnumerable<T> lets the consumer pull at its own pace, and a channel queues items between producers and consumers.

Checkpoint

Check: Declaring and raising an event

  1. Question 1 of 2 Inventory declares public event EventHandler<string>? LowStock. Which line compiles in code outside the Inventory class?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: inventory.LowStock += OnLow;

    The event keyword limits outside code to += and -=. Raising, assigning and clearing stay with the class that declares the event.

  2. Question 2 of 2 In inventory.cs, suppose the dashboard subscribes before the reorder service. What does the raise at 3 units print first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The dashboard's line

    An event is a multicast delegate: one raise calls every handler, in the order of +=.

A subscription keeps the subscriber alive until it is removed

A subscription: A handler added to an event with +=. It lasts until the same delegate is removed with -=, and while it lasts the publisher keeps the subscriber reachable, so it cannot be collected. is the handler that += stored in the publisher, and the handler’s delegate refers to its target, the subscriber, so the publisher keeps the subscriber reachable while the handler stays. Microsoft’s introduction to events calls this a coupling that the subscriber must end when it is no longer interested. subscriptions.cs adds and removes handlers three ways:

subscriptions.cs: three subscriptions, removed three ways C# · subscriptions/subscriptions.cs
// A subscription is a reference from the publisher to the subscriber's handler, and it lasts until the handler is
// removed with -=. The products are made up for this lesson.
var inventory = new Inventory();

// -= with a second lambda of the same text removes nothing: the two lambdas are two different delegates.
inventory.LowStock += (sender, sku) => Console.WriteLine($"  refresh for {sku}");
inventory.LowStock -= (sender, sku) => Console.WriteLine($"  refresh for {sku}");
Console.WriteLine($"after -= with a new lambda: {inventory.Handlers} handler");

// Keep the delegate that was added, and remove exactly that one.
EventHandler<string> badge = (sender, sku) => Console.WriteLine($"  badge for {sku}");
inventory.LowStock += badge;
Console.WriteLine($"after += badge: {inventory.Handlers} handlers");
inventory.LowStock -= badge;
Console.WriteLine($"after -= badge: {inventory.Handlers} handler");

// A subscriber that adds its handler when it opens and removes it when it is disposed.
using (var screen = new StockScreen(inventory))
{
    Console.WriteLine($"screen open: {inventory.Handlers} handlers; the last one runs on a {inventory.LastTarget}");
    inventory.Raise("TEA-250");
}

Console.WriteLine($"screen disposed: {inventory.Handlers} handler");
inventory.Raise("RICE-5");

class Inventory
{
    public event EventHandler<string>? LowStock;

    public int Handlers => LowStock?.GetInvocationList().Length ?? 0;

    public string LastTarget => LowStock?.GetInvocationList()[^1].Target?.GetType().Name ?? "none";

    public void Raise(string sku)
    {
        Console.WriteLine($"{sku} is low:");
        LowStock?.Invoke(this, sku);
    }
}

sealed class StockScreen : IDisposable
{
    private readonly Inventory _inventory;

    public StockScreen(Inventory inventory)
    {
        _inventory = inventory;
        _inventory.LowStock += OnLowStock;
    }

    // The method group OnLowStock makes an equal delegate each time, so -= finds and removes the one += added.
    public void Dispose() => _inventory.LowStock -= OnLowStock;

    private void OnLowStock(object? sender, string sku) => Console.WriteLine($"  stock screen shows {sku}");
}

Output

after -= with a new lambda: 1 handler
after += badge: 2 handlers
after -= badge: 1 handler
screen open: 2 handlers; the last one runs on a StockScreen
TEA-250 is low:
  refresh for TEA-250
  stock screen shows TEA-250
screen disposed: 1 handler
RICE-5 is low:
  refresh for RICE-5

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run subscriptions.cs

The first -= removed nothing: a new lambda with the same text is a different delegate, so the refresh handler stayed and still ran for RICE-5. badge, kept in a variable, came off at once. StockScreen subscribes its method OnLowStock when it opens and removes it in Dispose; the method group gives an equal delegate, so -= found it. Step through what the inventory holds:

Interactive · step by step What the inventory holds while a screen is subscribed Step through the screen's life in subscriptions.cs. Watch the references from the inventory to its subscribers.
What the inventory holds while a screen is subscribed, step 5 of 5. Showing Inventory, LowStock handlers (1 handler), refresh lambda, StockScreen (disposed, can be collected). In focus: refresh lambda.Inventorylives as long as the appLowStock handlers1 handlerrefresh lambdanever removedStockScreendisposed, can be collectedRICE-5

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The leftover lambda. The inventory's event already holds the refresh lambda: -= with a new lambda of the same text did not remove it, so 1 handler is left.
  2. The screen subscribes. new StockScreen(inventory) runs LowStock += OnLowStock. The event now holds a delegate whose target is the screen: 2 handlers.
  3. The event reaches both. Raise("TEA-250") calls the refresh lambda and then the screen's handler, in the order they were added.
  4. Dispose removes the handler. The using block ends and Dispose runs LowStock -= OnLowStock. The method group gives an equal delegate, so -= finds it: the inventory no longer refers to the screen.
  5. Only the leftover remains. Raise("RICE-5") reaches only the refresh lambda. Had the screen not unsubscribed, the inventory would have kept it alive and run its handler too.

This is how events cause memory leaks. A screen that subscribes to a long-lived inventory and never unsubscribes stays in memory after it closes, and keeps redrawing on every event; a static event is the worst case. Such memory leaks show no error, only memory that grows.

Removing a lambda with a copy of it

inventory.LowStock -= (sender, sku) => Refresh(sku); compiles, runs and removes nothing, because it builds a new delegate that was never added. Keep the handler in a field or a variable, or subscribe a method, and remove that one.

There are three ways for subscriptions to end. Pair every += with a -= in the subscriber’s Dispose. Return the subscription from a Subscribe method as an IDisposable, so that using ends it. Or keep the subscriber only through a weak reference, so a forgotten one can still be collected.

Raising has its own cases. A handler that throws stops the handlers after it, unless the raise calls each handler of GetInvocationList() in its own try. Handlers can also be removed, or added from other threads, while a raise runs.

Key idea

Subscriptions last until they are removed: an event holds every subscriber’s handler, and through it the subscriber, so remove exactly the delegate you added when the subscriber’s life ends.

Checkpoint

Check: How long a subscription keeps its subscriber

  1. Question 1 of 2 button.Click += (s, e) => Save(); and later button.Click -= (s, e) => Save(); No other handler was added. How many handlers does Click have now?

    Type a number.

    A hint

    Ask whether the delegate given to -= is the one that += added.

    Show the answer: Question 1 of 2

    Answer: 1 handlers

    The two lambdas are two delegates, so -= finds nothing to remove and does nothing, as the first -= in subscriptions.cs did. One handler is left, and it still runs.

  2. Question 2 of 2 A screen subscribes its method to a static App.Changed event and is closed without unsubscribing. What keeps the screen in memory?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The static event holds a delegate whose target is the screen

    A subscription is a reference from the publisher to the subscriber. It ends only when the handler is removed.

Ending subscriptions: a leak measured, IDisposable tokens and weak handlers Pro: opens with a Pro, Premium or Ultimate pass

A forgotten handler raises no error: the subscriber simply never goes away. The first run measures that on a static event, and the next two end subscriptions in ways that do not depend on remembering a -=.

In this part:

  • Three closed screens kept alive by a static event, measured
  • A subscription as an IDisposable, which using ends
  • Weak handlers, for publishers that cannot know their subscribers’ lives

3 runnable examples (C#) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Raising events safely: failing handlers, changes during a raise, threads and accessors Pro: opens with a Pro, Premium or Ultimate pass

Raising an event runs code that the publisher did not write. These runs show what one failing handler does to the others, what happens when the list changes during a raise, and how accessors control who may subscribe.

In this part:

  • One failing handler stops the rest, and GetInvocationList isolates them
  • Removing a handler during a raise, and subscribing from many threads
  • async handlers, custom accessors and C# 14 partial events
  • Events, observables, async streams or channels

3 runnable examples (C#) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: a price alert that ends its own subscriptions

price_alert.cs checks a PriceAlert that raises ThresholdCrossed only when a price crosses its threshold, and whose Subscribe returns an IDisposable. All nine checks fail on the starter:

price_alert.cs: nine checks of a price alert C# · practice/price_alert.cs
// Practice: write PriceAlert, then run this file until all nine checks pass. What it must do is in the comments of
// the class. The products and prices are made up for this exercise.
(string Check, Func<string> Run, string Expected)[] checks =
[
    ("the first price only sets the start", () => Alerts(500, [480]), "none"),
    ("crossing upwards raises Up", () => Alerts(500, [480, 520]), "Up 520"),
    ("staying above raises nothing more", () => Alerts(500, [480, 520, 530, 560]), "Up 520"),
    ("crossing downwards raises Down", () => Alerts(500, [520, 495]), "Down 495"),
    ("reaching the threshold counts as above", () => Alerts(500, [480, 500, 499]), "Up 500, Down 499"),
    ("every subscriber hears a crossing", TwoSubscribers, "A: Up 510, B: Up 510"),
    ("disposing the subscription stops alerts", DisposeStops, "Up 520, then nothing"),
    ("disposing twice is harmless", DisposeTwice, "no error"),
    ("Subscribe refuses null", () => Outcome(() => { new PriceAlert("TEA-250", 500).Subscribe(null!); return "accepted"; }), "throws ArgumentNullException"),
];

int passed = 0;
foreach (var (check, run, expected) in checks)
{
    string got = Outcome(run);
    bool ok = got == expected;
    if (ok)
    {
        passed++;
    }

    Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, {got} instead of {expected}");
}

Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;

static string Alerts(decimal threshold, decimal[] prices)
{
    var alert = new PriceAlert("TEA-250", threshold);
    var seen = new List<string>();
    alert.Subscribe((sender, e) => seen.Add($"{e.Direction} {e.Price}"));
    foreach (decimal price in prices)
    {
        alert.Update(price);
    }

    return seen.Count == 0 ? "none" : string.Join(", ", seen);
}

static string TwoSubscribers()
{
    var alert = new PriceAlert("TEA-250", 500);
    var seen = new List<string>();
    alert.Subscribe((sender, e) => seen.Add($"A: {e.Direction} {e.Price}"));
    alert.Subscribe((sender, e) => seen.Add($"B: {e.Direction} {e.Price}"));
    alert.Update(490);
    alert.Update(510);
    return string.Join(", ", seen);
}

static string DisposeStops()
{
    var alert = new PriceAlert("TEA-250", 500);
    var seen = new List<string>();
    IDisposable subscription = alert.Subscribe((sender, e) => seen.Add($"{e.Direction} {e.Price}"));
    alert.Update(480);
    alert.Update(520);
    subscription.Dispose();
    alert.Update(470);
    return $"{string.Join(", ", seen)}, then {(seen.Count == 1 ? "nothing" : "more")}";
}

static string DisposeTwice()
{
    var alert = new PriceAlert("TEA-250", 500);
    IDisposable subscription = alert.Subscribe((sender, e) => { });
    subscription.Dispose();
    subscription.Dispose();
    return "no error";
}

static string Outcome(Func<string> run)
{
    try
    {
        return run();
    }
    catch (Exception e)
    {
        return $"throws {e.GetType().Name}";
    }
}

enum Direction
{
    Up,
    Down,
}

record ThresholdCrossedEventArgs(string Sku, decimal Price, Direction Direction);

class PriceAlert(string sku, decimal threshold)
{
    // Raise this event when a new price crosses the threshold: from below it to at or above it (Up), or from at or
    // above it to below it (Down). The first price only sets the starting side and raises nothing.
    public event EventHandler<ThresholdCrossedEventArgs>? ThresholdCrossed;

    public string Sku { get; } = sku;

    public decimal Threshold { get; } = threshold;

    // Records a new price and raises ThresholdCrossed when it crosses the threshold.
    public void Update(decimal price) => throw new NotImplementedException();

    // Adds the handler to ThresholdCrossed and returns an IDisposable whose Dispose removes it. Disposing again does
    // nothing. A null handler throws ArgumentNullException.
    public IDisposable Subscribe(EventHandler<ThresholdCrossedEventArgs> handler) => throw new NotImplementedException();
}

Output (exit status 1)

the first price only sets the start: FAIL, throws NotImplementedException instead of none
crossing upwards raises Up: FAIL, throws NotImplementedException instead of Up 520
staying above raises nothing more: FAIL, throws NotImplementedException instead of Up 520
crossing downwards raises Down: FAIL, throws NotImplementedException instead of Down 495
reaching the threshold counts as above: FAIL, throws NotImplementedException instead of Up 500, Down 499
every subscriber hears a crossing: FAIL, throws NotImplementedException instead of A: Up 510, B: Up 510
disposing the subscription stops alerts: FAIL, throws NotImplementedException instead of Up 520, then nothing
disposing twice is harmless: FAIL, throws NotImplementedException instead of no error
Subscribe refuses null: FAIL, throws NotImplementedException instead of throws ArgumentNullException
0 of 9 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run price_alert.cs

Write PriceAlert

Copy price_alert.cs. Keep the side of the last price in a bool? that starts null, and raise the event in Update only when the new side differs from a known old one. In Subscribe, check the handler with ArgumentNullException.ThrowIfNull, add it, and return an object whose Dispose removes it once. Run dotnet run price_alert.cs until it prints 9 of 9 checks pass.

Then check yourself on declaring, raising and removing handlers:

Practice that adapts to you

Practise this lesson

6 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 Which declaration follows the standard .NET event pattern for a low-stock notice?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: public event EventHandler<LowStockEventArgs>? LowStock;

    EventHandler<TEventArgs> gives every handler the sender and the event's data; the event keyword keeps raising with the class.

  2. Question 2 of 6 Nobody has subscribed to LowStock yet. What does LowStock?.Invoke(this, e) do?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: Nothing: the event is null, and ?. skips the call

    A publisher must work with no listeners at all, which is why the pattern raises with ?.Invoke.

  3. Question 3 of 6 A sorting method cannot work without a comparison from its caller. Should it take a delegate parameter or offer an event?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: A delegate parameter, because the method must call the caller's code to do its job

    Listening to an event is optional, so a design that needs an answer back takes a delegate parameter instead.

  4. Question 4 of 6 A screen subscribes to an inventory's LowStock in its constructor. Where should it remove its handler?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: In Dispose, or wherever the screen's life ends

    A subscription lasts until -= removes it, so remove it where the subscriber's life ends.

  5. Question 5 of 6 Why did -= OnLowStock in StockScreen.Dispose remove the handler, while -= with a second lambda did not?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Two conversions of the same method on the same object give equal delegates, but two lambdas are two methods

    Delegate equality is by target and method. Keep the delegate you added, or use a method, so that -= can find it.

  6. Question 6 of 6 A static event is raised once a second. Screens subscribe a method when they open and never unsubscribe. After 50 screens were opened and closed, how many handlers run on each raise?

    Type a number.

    Show the answer: Question 6 of 6

    Answer: 50 handlers

    Every screen left its handler on the event, and the static event keeps them all: 50 handlers a second, for screens the user closed, and the screens stay in memory with them.

Interview questions

Fresher · What is the difference between a public delegate field and an event?

Both hold a delegate with any number of handlers; the difference is what outside code may do. A public delegate field can be called by anyone, raising the notification, and assigned with =, which throws away every other subscriber. An event lets outside code only add and remove its own handler with += and -=; raising and clearing stay with the declaring class, and the compiler refuses the rest with CS0070. The class keeps control of when the notification happens, and subscribers cannot disturb each other.

  1. Mid to senior

    A payment service raises PaymentCompleted with five subscribers. An analytics handler starts throwing during an outage, and from then on the audit handler, subscribed after it, records no payments, although the payments succeed. Explain why, and redesign the raise without losing the analytics errors.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A background thread raises StockChanged, and a screen removes its handler in Dispose on the UI thread. Now and then the screen's handler runs after Dispose and throws ObjectDisposedException. The raise uses ?.Invoke. Explain why that does not prevent it, and fix it.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A market-data service receives 20,000 price ticks a second and must feed a chart, a risk engine that sometimes pauses for half a second, and an archive that needs to know when the feed ends. Would you expose an event, IObservable<T>, IAsyncEnumerable<T> or a channel, and why?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An event is a delegate that only its class can raise: outside code can only add and remove handlers, and the standard pattern is EventHandler<TEventArgs>, raised once with ?.Invoke(this, e).
  • An event keeps every subscriber’s handler, and through it the subscriber, until the handler is removed.
  • Remove exactly the delegate you added: a method or a stored delegate, never a second lambda with the same text.

Terms in this lesson

event
A member, declared with the event keyword, that holds handlers of a delegate type. Code outside its class can only add and remove handlers with += and -=; only the declaring class can raise it.
subscription
A handler added to an event with +=. It lasts until the same delegate is removed with -=, and while it lasts the publisher keeps the subscriber reachable, so it cannot be collected.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.