Skip to main content
    Courses/C#/Deep Dive Into Delegates

    Lesson 16 • Advanced Track

    Deep Dive Into Delegates & Multicast Delegates

    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.

    What You'll Learn

    • Build a multicast delegate and inspect its invocation list
    • Add and remove targets with += / -= (Delegate.Combine and Remove)
    • Understand why a multicast Func only returns the LAST result
    • Compose Func<> delegates into reusable pipelines
    • Explain closures — how a lambda captures variables, not values
    • Tell delegates, lambdas, and method groups apart

    💡 Real-World Analogy

    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.

    📊 Delegate Toolkit Quick Reference

    ToolWhat it doesExample
    +=Add a method to the chainlog += FileLog;
    -=Remove a method from the chainlog -= FileLog;
    GetInvocationList()Inspect every method in orderlog.GetInvocationList()
    Delegate.CombineWhat += calls under the hoodDelegate.Combine(a, b)
    Delegate.RemoveWhat -= calls under the hoodDelegate.Remove(a, b)
    Func<int,int>A delegate returning a valuex => x * 2

    += 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.

    Worked example: one delegate, three methods

    Watch += build the chain, -= shrink it, and GetInvocationList() show what's inside.

    Try it Yourself »
    C#
    using System;
    
    // A delegate type for any "do something with a string" method.
    delegate void Logger(string message);
    
    class Program
    {
        static void ConsoleLog(string msg) => Console.WriteLine($"[CONSOLE] {msg}");
        static void FileLog(string msg)    => Console.WriteLine($"[FILE]    {msg}");  // simulated
        static void AlertLog(string msg)   => Console.WriteLine($"[ALERT]   {msg}");
    
        static void Main()
        {
            // Every delegate in C# is MULTICAST: it can hold many methods at once.
    ...

    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.

    🎯 Your turn: combine and uncombine methods

    Use += to add a target and -= to remove it; check the output matches.

    Try it Yourself »
    C#
    using System;
    
    class Program
    {
        static void SayHi(string name)  => Console.WriteLine($"Hi {name}!");
        static void SayBye(string name) => Console.WriteLine($"Bye {name}!");
    
        static void Main()
        {
            // 🎯 YOUR TURN — build a multicast delegate. Replace each ___.
    
            // 1) Start the chain pointing at SayHi (note: NO parentheses).
            Action<string> greeters = ___;        // 👉 SayHi
    
            // 2) ADD SayBye to the same delegate with the += operator.
            greeters ___
    ...

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

    Here's the gotcha that surprises everyone. A Func<T> 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.

    Worked example: only the last result survives

    Three methods run, but roll() returns just 3 — then collect them all manually.

    Try it Yourself »
    C#
    using System;
    
    class Program
    {
        static void Main()
        {
            // A Func<T> CAN be multicast too — but watch the return value.
            Func<int> roll = () => 1;
            roll += () => 2;
            roll += () => 3;
    
            // All three run, but you only get the LAST one's result back.
            int result = roll();
            Console.WriteLine($"roll() returned: {result}");   // 3  (1 and 2 are thrown away!)
    
            // If you actually want every result, walk the invocation list yourself.
            C
    ...

    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 Func<int,int> 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.

    🎯 Your turn: compose two Func delegates

    Run doubleIt first, then addThree on its result; the answer should be 13.

    Try it Yourself »
    C#
    using System;
    
    class Program
    {
        static void Main()
        {
            // 🎯 YOUR TURN — COMPOSE two Func<int,int> into a pipeline.
            // Composition means: feed the output of one into the next.
    
            Func<int, int> doubleIt = x => x * 2;   // takes an int, returns an int
            Func<int, int> addThree = x => x + 3;
    
            // 1) Build "compose": run doubleIt FIRST, then addThree on its result.
            Func<int, int> compose = x => addThree(___);   // 👉 doubleIt(x)
    
            // 2) Invoke 
    ...

    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.

    Worked example: a lambda sees live variables

    Change a captured variable after the fact and watch the lambda's output change too.

    Try it Yourself »
    C#
    using System;
    using System.Collections.Generic;
    
    class Program
    {
        static void Main()
        {
            // A lambda CAPTURES the variables it uses from the surrounding scope.
            // It captures the VARIABLE, not a snapshot of its value.
            int factor = 10;
            Func<int, int> scale = n => n * factor;   // 'factor' is captured
    
            Console.WriteLine(scale(5));   // 50
    
            factor = 100;                  // change the captured variable...
            Console.WriteLine(scale(5));   //
    ...

    🔎 Deep Dive: delegate vs lambda vs method group

    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.

    Func<int,int> a = x => x * 2;        // lambda           -> creates a delegate
    Func<string> b = DateTime.Now.ToString;  // method group -> converts to a delegate
    Action c = Console.WriteLine;        // method group conversion (no parentheses!)
    
    // All three are delegate INSTANCES once assigned. Add parentheses to INVOKE:
    a(5);   // 10
    c();    // prints a blank line

    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.

    Pro Tips

    • 💡 Keep multicast delegates to Action: because only the last Func result survives, use void methods when you chain — and walk GetInvocationList() if you truly need every result.
    • 💡 Use a named method (or stored lambda) if you'll -= later: d -= someName; works, but d -= x => ...; removes nothing — the new lambda is a different instance.
    • 💡 Capture a fresh copy in loops: declare int copy = i; inside a for loop before capturing, so each lambda gets its own value.
    • 💡 Cache before invoking for thread safety: var handler = OnChanged; handler?.Invoke(); avoids a race where another thread clears the delegate mid-call.
    • 💡 += builds a new delegate: delegates are immutable — combining returns a fresh instance, so the original variable is reassigned, not mutated in place.

    Common Errors (and the fix)

    • "My multicast Func only returns one value": that's by design — an invoked multicast Func<T> hands back only the last method's result; the rest are discarded. Use Action for chains, or loop over GetInvocationList() and call each yourself to collect every result.
    • "My -= didn't remove anything": removing a handler that was never added is a silent no-op. With lambdas this bites hard — d += x => f(x); then d -= x => f(x); removes nothing because each lambda is a brand-new object. Store the lambda in a variable and pass that same reference to both.
    • "System.NullReferenceException" when invoking: an empty (unassigned) delegate is null, and calling d(...) throws. Guard with the null-conditional operator: d?.Invoke(...); simply does nothing when the list is empty.
    • "All my loop lambdas print the same number": they captured the same loop variable, not its value at the time. Fix it by copying inside the loop: int copy = i; and capture copy — each iteration then closes over its own variable.
    • "CS0123: No overload for 'Method' matches delegate 'D'": the method group you tried to add doesn't match the delegate's signature. Check the parameter types and return type line up exactly before +=-ing it onto the chain.

    📋 Quick Reference

    TaskCodeNotes
    Add a methodlog += FileLog;Calls Delegate.Combine
    Remove a methodlog -= FileLog;No-op if not present
    Count the chainlog.GetInvocationList().LengthNumber of methods
    Invoke safelylog?.Invoke("hi");Skips if null
    Compose Funcsx => g(f(x))Pipe one into the next
    Multicast Func resultint r = roll();Last method's value only
    Capture safely in a loopint copy = i;Fresh variable each pass

    Frequently Asked Questions

    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.

    Q: Is a lambda the same as a delegate?

    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.

    Mini-Challenge: a Handler Pipeline

    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.

    🎯 Mini-Challenge: chain three handlers

    Combine three lambdas onto one Action and fire them all with a single call.

    Try it Yourself »
    C#
    using System;
    
    class Program
    {
        static void Main()
        {
            // 🎯 MINI-CHALLENGE: a text-processing pipeline
            // 1. Make an  Action<string>  called  pipeline.
            // 2. CHAIN three handlers onto it with += , each printing one step:
            //      - "Step 1: received '{text}'"
            //      - "Step 2: length is {text.Length}"
            //      - "Step 3: upper is {text.ToUpper()}"
            //    (you can use lambdas:  pipeline += t => Console.WriteLine(...);)
            // 3. Invo
    ...

    🎉 Lesson Complete

    • ✅ Every C# delegate is multicast — it holds an ordered invocation list of methods
    • += adds (Delegate.Combine) and -= removes (Delegate.Remove); removing a non-added handler is a no-op
    • ✅ A multicast Func<T> runs every method but returns only the last result
    • ✅ Compose Func delegates into pipelines by nesting calls: x => g(f(x))
    • ✅ Lambdas capture variables, not values — copy loop variables to avoid the classic trap
    • ✅ A delegate is the container; a lambda and a method group are two ways to fill it
    • Next lesson: Events — advanced patterns and custom EventArgs

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service