Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to pass behaviour around

Virtual methods, interfaces, delegates, Func and Action, lambdas, events and the service container. How to hand code a piece of behaviour it doesn't write itself.

Ask what the thing you're handing over is.

  • One operation, supplied at the call site (a filter, a sort key, a discount rule) → a Func or Action, usually written as a lambda: orders.Where(o => o.Total > 50).
  • Behaviour with a name, several related members, or its own settings (a shipping rule with Cost and Describe, a payment gateway) → an interface, with one class per variant. If you'd write a class whose only member is one method, a Func is probably enough.
  • Variants that share real code and state and form a genuine "is a kind of" family → a base class with virtual or abstract members. Prefer an interface plus composition when the only thing shared is the promise.
  • Something happened and any number of listeners may care (a download finished, stock ran low) → an event. Only the owner can raise it; subscribers can only add and remove handlers.
  • A whole app's worth of services that depend on each other → constructor injection, wired up by the service container.

A lambda that uses a local variable captures the variable itself, not its value at the time. That's usually what you want, until it's inside a loop.

Through the type system

The base class says what must happen; each derived class says how.

"Can do" rather than "is a kind of". Any class or struct can sign up.

Swap the behaviour without touching the code that uses it, and hand a test a stand-in.

Methods as values

The type underneath Func, Action and events.

The built-in delegate types, and the test for choosing a Func over an interface.

Write the method inline with =>, or pass an existing one by name.

Formal. A lambda or local function that uses a local variable from its enclosing scope captures that variable itself. The lambda reads the variable's current value each time it runs and can assign to it; the variable's lifetime is extended as long as the lambda is reachable. A foreach loop variable is a fresh variable per iteration, while a for loop's counter is one variable shared by every iteration.

In plain English. A lambda doesn't take a snapshot. It keeps a line to the variable, so if the variable changes before the lambda runs, the lambda sees the change.

Example. Adding () => i in a for (int i = 0; i < 3; i++) loop and calling the lambdas afterwards gives 3, 3, 3; copying int copy = i; inside the loop and capturing copy gives 0, 1, 2.

Why this rule exists. Capturing the variable is what lets a lambda share state with the code around it, which is what makes callbacks and counters work.

A lambda captures the variable, not a snapshot of its value.

Notifying and wiring

Many listeners, one owner who raises it, and why you unsubscribe.

Formal. A delegate instance can hold an invocation list of several methods; += and -= add and remove targets, and invoking the delegate calls them in order. Declaring a delegate field with the event keyword restricts code outside the declaring type to += and -=: only the declaring type can invoke (raise) the event or replace its list (error CS0070 otherwise).

In plain English. A delegate is a list of methods to call. An event is that list with a lock on it: anyone can sign up or leave, but only the owner can ring the bell.

Example. button.Clicked += OnClicked; compiles anywhere; button.Clicked(this, EventArgs.Empty); compiles only inside Button.

Why this rule exists. Without the restriction, any subscriber could fire the event or wipe out every other subscriber by assigning with =.

Outside the class, an event allows only += and -=. Nobody else can raise it or wipe the list.

Register an interface and its implementation once, and the container hands it to every constructor that asks.