C# delegates & events
What a delegate is, and Func vs Action vs Predicate
A delegate is a type-safe reference to a method: a value that holds a method and can be called, passed around, or stored like any other variable. Writing public delegate int Calculator(int x, int y); defines a new reference type, and the compiler checks that anything you assign to it matches that signature.
Most code never declares its own delegate type, because the framework ships generic ones:
Action<string> log = Console.WriteLine; // returns void, up to 16 params
Func<int, int, int> add = (a, b) => a + b; // last type arg is the RETURN type
Predicate<int> isEven = n => n % 2 == 0; // same shape as Func<int, bool>
int sum = add(2, 3); // 5 (sugar for add.Invoke(2, 3))
Action returns void. Func's final type argument is the return type, so Func<int, string, bool> takes an int and a string and returns a bool. Predicate<T> names a test and matches Func<T, bool>. Assign a method by its name with no parentheses (Console.WriteLine); parentheses would call it and assign the result.
Multicast delegates: += builds an invocation list
Every delegate can hold more than one method. += adds a target, -= removes one, and calling the delegate runs each target synchronously in the order they were added.
Action notify = () => Console.WriteLine("email");
notify += () => Console.WriteLine("sms");
notify(); // prints email then sms
Delegates are immutable, so += does not change the original; it assigns a new delegate whose invocation list is the old one plus the new target. Remove the last target and the delegate becomes null. Two behaviors surprise people and live in the senior notes: the call returns only the last target's value, and one target that throws stops the rest.
Events wrap a delegate so only the owner can raise it
An event is a delegate field with guardrails. Outside code can subscribe (+=) and unsubscribe (-=), but cannot assign it with = or invoke it; only the declaring type can raise it.
public event EventHandler<OrderEventArgs>? Placed;
protected virtual void OnPlaced(OrderEventArgs e)
=> Placed?.Invoke(this, e); // ?. guards the zero-subscriber case
The standard signature is (object sender, TEventArgs e) returning void, with data carried in a class derived from EventArgs (use EventArgs.Empty when there is none). The ?. earns its place: with no subscribers the backing field is null, and calling Placed(this, e) directly would throw a NullReferenceException.
Wrong "an event is the same as a public delegate field with a different keyword." A public field lets any caller reassign the whole list with = or invoke it. The event keyword compiles to add and remove accessors that expose only += and -=, so no subscriber can clobber another and no outsider can raise it.
Lambdas capture variables by reference
A lambda can read locals from the method around it. It captures the variable itself, not a snapshot of its value, so the value is read when the lambda runs.
int factor = 2;
Func<int, int> scale = x => x * factor;
factor = 10;
Console.WriteLine(scale(5)); // 50 (factor is read at call time)
Capturing also extends the variable's lifetime: it stays alive for as long as the lambda does. A static lambda opts out, refusing to capture any local or instance state, which rules out an accidental capture allocation.
Why a multicast delegate returns only the last value
Invoking a multicast delegate runs every target, but a single call can carry back only one return value, and the language keeps the last target's.
Func<int> chain = () => 1;
chain += () => 2;
chain += () => 3;
int result = chain(); // 3 (all three run; the 1 and 2 are discarded)
The same rule covers out and ref parameters: every target receives a reference to the same variable, so a later target sees what an earlier one wrote, and the value left afterward is the last target's. To keep every result, walk the invocation list yourself:
foreach (Func<int> f in chain.GetInvocationList())
Console.WriteLine(f()); // 1, then 2, then 3
One throwing handler aborts the rest
An uncaught exception in a target propagates immediately, and no later target on the invocation list runs.
Action steps = () => Console.WriteLine("A");
steps += () => throw new InvalidOperationException("boom");
steps += () => Console.WriteLine("C");
steps(); // prints A, then the exception propagates; C never runs
Wrong "the other handlers still run and the exception gets logged somewhere." Invocation is a synchronous loop over the list, so a throw exits it the way a throw exits any loop. To isolate faults, loop GetInvocationList() with a try/catch around each call, and the survivors run regardless of one bad handler.
Raising a base-class event needs a protected virtual method
An event can be raised only by the type that declares it, so a derived class cannot fire a base-class event directly. The pattern is a protected virtual OnX method the derived class calls or overrides.
public abstract class Shape {
public event EventHandler<ShapeEventArgs>? Changed;
protected virtual void OnChanged(ShapeEventArgs e) => Changed?.Invoke(this, e);
}
public class Circle : Shape {
protected override void OnChanged(ShapeEventArgs e) {
// circle-specific work first
base.OnChanged(e); // REQUIRED, or subscribers never hear it
}
}
Make the method virtual, not the event. The docs warn that a virtual event overridden in a derived class is mishandled by the compiler, and whether a subscriber attaches to the base or the derived event becomes unpredictable.
The lapsed-listener leak, and the lambda you cannot remove
A subscription is a reference. The publisher's invocation list points at the subscriber's handler, which points at the subscriber, so while a long-lived publisher holds the subscription the subscriber cannot be collected. Forgetting -= before you drop a subscriber is the lapsed-listener leak.
publisher.Tick += subscriber.OnTick; // publisher now roots subscriber
publisher.Tick -= subscriber.OnTick; // release it before disposing
Garbage collection offers no rescue here: reachability through the publisher is what keeps the object alive. One trap sharpens the problem. Subscribe with an inline lambda and you cannot reliably -= it later, because a freshly written identical lambda is a different delegate instance, and the removal silently finds nothing. Store the lambda in a field and pass that same instance to both += and -=, or reach for a weak event pattern so the publisher holds only a weak reference.
for and foreach capture the loop variable differently
A for loop reuses one iteration variable across all passes, so every lambda that closes over it reads the final value.
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
actions.Add(() => Console.Write(i));
foreach (var a in actions) a(); // prints 333
The fix is a fresh local inside the loop body: int copy = i;, then capture copy, which prints 012. In current C# a foreach variable is a fresh binding each iteration, so capturing it is safe; for was left sharing one variable, which is why the two loops behave differently.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.