Delegates Deep

By the end of this lesson you'll be able to chain several methods onto a single delegate, manage that invocation list with += and -= , compose Func pipelines, and avoid the two traps that bite everyone: multicast return values and closures over captured loop variables.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

A multicast delegate is a group chat . When you post one message, it fans out to everyone currently in the group — that ordered list of members is the invocation list . You can add a member ( += ) or remove one ( -= ) at any time, and the next message goes to whoever's in the group right then. The twist: if you ask the group "what's 2+2?" and three people reply, you only hear the last answer — which is exactly why a multicast Func hands back only the final return value. And a closure is a member who keeps a live link to a shared whiteboard: if you update the whiteboard later, they read the new value, not the old one.

+= and -= are just friendly syntax for Delegate.Combine and Delegate.Remove — they build a brand-new combined delegate rather than mutating the old one.

1. Multicast Delegates & the Invocation List

In C#, every delegate is a multicast delegate — it can hold a whole list of methods, not just one. That ordered list is called the invocation list . You add a method with += and remove one with -= ; when you call the delegate, every method in the list runs in turn, in the order they were added. You can peek at the list any time with GetInvocationList() . Read this worked example, run it, then you'll build a chain yourself.

Your turn. Build a multicast delegate from scratch: point it at one method, += a second, invoke it, then -= the second back off and invoke again. Fill in the three ___ blanks using the hints, then run it.

2. Multicast Return Values — the Last-One-Wins Trap

Here's the gotcha that surprises everyone. A can be multicast just like an Action — but when you invoke it, all the methods run, yet you only get back the return value of the last one. Every earlier result is silently discarded. That's why multicast delegates are almost always built from Action (void) methods. If you genuinely need every result, walk the invocation list yourself.

3. Composing Func Pipelines

Multicast chaining runs methods side-by-side and throws away their results — but often you want the opposite : feed the output of one function into the next. That's composition . With delegates you build a small pipeline by wrapping one call inside another: x => second(first(x)) . Each step transforms the value and passes it along. Try it yourself below.

Fill in the two ___ blanks to chain doubleIt into addThree , then invoke the composed pipeline.

4. Closures — Capturing Variables, Not Values

When a lambda uses a variable from the surrounding code, it captures it — and this is the subtle part: it captures the variable itself , not a copy of its current value. So if that variable changes later, the lambda sees the new value when it runs. This is brilliant for building configurable functions, and a notorious foot-gun inside loops. Read the worked example and watch both behaviours.

These three words get muddled constantly. A delegate is the type (and the object) that holds one or more methods — it's the container. A lambda ( x => x * 2 ) is one concise way to create a method inline that fills that container. A method group is simply the name of an existing method, with no parentheses — like Console.WriteLine — which C# can also convert into a delegate.

Rule of thumb: write a lambda for throwaway inline logic, pass a method group when a suitable named method already exists, and remember both end up as the same thing — a delegate instance.

Q: If a multicast Func throws away earlier results, how do I get them all?

Loop over GetInvocationList() and invoke each entry yourself: foreach (Func int f in roll.GetInvocationList()) results.Add(f()); . That way you collect every method's return value instead of just the last.

Q: What's actually happening when I write d += handler ?

The compiler turns it into d = (T)Delegate.Combine(d, handler); . Delegates are immutable, so this creates a new combined delegate and reassigns d — the old delegate object is untouched. -= works the same way via Delegate.Remove .

Q: Why do all my lambdas in a loop print the same value?

They all captured the same loop variable, and by the time they run the loop has finished. Copy the value into a new variable inside the loop body ( int copy = i; ) and capture that — each iteration then closes over its own variable.

No — a lambda is a concise way to write a method inline; a delegate is the type/object that holds a method. C# converts the lambda into a delegate instance to match the target type. A method group (a method name with no parentheses) converts the same way.

No blanks this time — just a brief and an outline. Build one Action string delegate, chain three handlers onto it with += , and invoke it once so all three run in turn. This is the pattern behind logging pipelines, middleware, and event broadcasting in real apps. Run it and check your output against the expected lines.

Practice quiz

What does it mean that every delegate in C# is 'multicast'?

  • It can only hold one method
  • It runs methods on multiple threads
  • It can hold an ordered list of methods (the invocation list), all run when invoked
  • It broadcasts over a network

Answer: It can hold an ordered list of methods (the invocation list), all run when invoked. Every C# delegate is multicast: it can hold a whole invocation list of methods, run in order when the delegate is invoked.

What do += and -= do to a multicast delegate?

  • += adds a method to the chain; -= removes one
  • += replaces all methods; -= clears it
  • They only work on numeric delegates
  • Nothing — they are compile errors on delegates

Answer: += adds a method to the chain; -= removes one. += adds a method to the invocation list and -= removes one; under the hood they call Delegate.Combine and Delegate.Remove.

When you invoke a multicast Func<T> with several methods, which return value do you get?

  • An array of all results
  • The first method's result
  • The sum of all results
  • Only the last method's result; earlier ones are discarded

Answer: Only the last method's result; earlier ones are discarded. All methods run, but a multicast Func<T> returns only the last method's result — the rest are silently thrown away.

How can you collect every result from a multicast Func<int>?

  • Call it twice
  • Loop over GetInvocationList() and invoke each delegate yourself
  • Use the .Results property
  • It is impossible

Answer: Loop over GetInvocationList() and invoke each delegate yourself. Walk the invocation list with GetInvocationList() and call each Func yourself to gather every return value.

What do the operators += and -= actually call under the hood?

  • Delegate.Combine and Delegate.Remove
  • Delegate.Add and Delegate.Subtract
  • List.Add and List.Remove
  • Delegate.Append and Delegate.Pop

Answer: Delegate.Combine and Delegate.Remove. += compiles to Delegate.Combine and -= to Delegate.Remove; both produce a brand-new combined delegate rather than mutating the old one.

What does composing two Func<int,int> like x => addThree(doubleIt(x)) do?

  • Runs them side by side and discards results
  • Adds their return values
  • Feeds the output of doubleIt into addThree, forming a pipeline
  • Runs only addThree

Answer: Feeds the output of doubleIt into addThree, forming a pipeline. Composition feeds one function's output into the next: doubleIt runs first, then addThree on its result.

What does a lambda capture when it uses a variable from the surrounding scope?

  • A snapshot copy of the value at that moment
  • The variable itself, so it sees later changes to that variable
  • Nothing — lambdas can't use outer variables
  • Only constants

Answer: The variable itself, so it sees later changes to that variable. A closure captures the variable, not a snapshot of its value, so if the variable changes later the lambda sees the new value.

Why might all lambdas created in a loop print the same value?

  • Because lambdas are always identical
  • Because the loop runs only once
  • Because Console.WriteLine caches output
  • They captured the same loop variable; copy it into a fresh variable (int copy = i;) inside the loop to fix it

Answer: They captured the same loop variable; copy it into a fresh variable (int copy = i;) inside the loop to fix it. They all close over the same loop variable. Declaring a fresh 'int copy = i;' inside the loop gives each lambda its own captured value.

Why does 'd -= x => f(x);' often remove nothing?

  • Because -= is broken for lambdas
  • Each lambda literal is a different object, so it doesn't match the one you added; store it in a variable and pass that reference
  • Because lambdas can't be removed at all
  • Because f must be static

Answer: Each lambda literal is a different object, so it doesn't match the one you added; store it in a variable and pass that reference. Removing a handler that was never added is a silent no-op. A new lambda is a distinct instance, so store the lambda and pass the same reference to both += and -=.

How should you safely invoke a delegate that might be null (have no methods)?

  • Wrap it in a try/catch only
  • Call d.Invoke() and ignore the crash
  • Use the null-conditional operator: d?.Invoke(...)
  • Assign it to null first

Answer: Use the null-conditional operator: d?.Invoke(...). An empty/unassigned delegate is null and calling it throws. d?.Invoke(...) simply does nothing when the invocation list is empty.