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
FuncorAction, 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
CostandDescribe, a payment gateway) → an interface, with one class per variant. If you'd write a class whose only member is one method, aFuncis probably enough. - Variants that share real code and state and form a genuine
"is a kind of" family → a base class with
virtualorabstractmembers. 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.
Section
An interface is a contract
"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.
Section
Lambdas and method groups
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
Section
Events and the observer pattern
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.
Section
The service container
Register an interface and its implementation once, and the container hands it to every constructor that asks.