Skip to content

Instantly share code, notes, and snippets.

@John-Paul-R
Created November 18, 2023 17:34
Show Gist options
  • Save John-Paul-R/19bf4f76afb2868d91e874779674842d to your computer and use it in GitHub Desktop.
Save John-Paul-R/19bf4f76afb2868d91e874779674842d to your computer and use it in GitHub Desktop.
C#: Comparing combined Delegates and Events

I think what might clarify is how events work. (Note, this is a long answer, tailored to your example. For something shorter, but less specific, see https://stackoverflow.com/a/33964976/8105643)

The event keyword is really just a nice syntax for setting up an EventHandler Delegate.

Now we understand that events are just special syntax delegates, we can explain how subscribing to an event actually works, and show how the two examples you gave are actually quite similar:

In your first example, you have essentially this situation:

public class DelegateCombineExample {
    public static void Method1()
    {
        Console.WriteLine("Running Method 1");
    }

    public static void Method2()
    {
        Console.WriteLine("Running Method 2");
    }

    public void Main() {
        var action = Method1;
        action += Method2;
        action();
    }
}

The real question is what is += doing here?

Well, we can answer that! Here is a sharplab.io that shows the lowered c# code for the above block.

public delegate void MyDelegate();
public class DelegateCombineExample
{
    [CompilerGenerated]
    private static class <>O
    {
        public static Action <0>__Method1;

        public static Action <1>__Method2;
    }

    public static void Method1()
    {
        Console.WriteLine("Running Method 1");
    }

    public static void Method2()
    {
        Console.WriteLine("Running Method 2");
    }

    public void Main()
    {
        Action a = <>O.<0>__Method1 ?? (<>O.<0>__Method1 = new Action(Method1));
        a = (Action)Delegate.Combine(a, <>O.<1>__Method2 ?? (<>O.<1>__Method2 = new Action(Method2)));
        a();
    }
}

This looks complicated, but much of this is noise for us at the moment. The piece we're interested in is where did the += go? It's gone!

Instead, we have this new Delegate.Combine. This method call joins together the current delegate stored in action and combines it with Method2 when we do action += Method2.

(incidentally, there is a Delegate.Remove that gets called for -=).

So, now that we know that in the first example, "subscribing" really just means combining the delegates with Delegate.Combine, so that invoking action means invoking both delegates, how does example 2 work? Let's see!

In a fashion similar to the first, let's create a sharplab for example 2 to see what's going on:

public delegate void MyDelegate();

public class EventExample {
    public event MyDelegate MyEvent;

    public static void Method1()
    {
        Console.WriteLine("Running Method 1");
    }

    public void M() {
        MyEvent += Method1;
        MyEvent();
    }
}

This time I'll jsut take an excerpt from the generated code, since there's a lot:

public class EventExample
{
    [CompilerGenerated]
    private static class <>O
    {
        public static MyDelegate <0>__Method1;
    }

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private MyDelegate m_MyEvent;

    public event MyDelegate MyEvent
    {
        [CompilerGenerated]
        add
        {
            MyDelegate myDelegate = this.MyEvent;
            while (true)
            {
                MyDelegate myDelegate2 = myDelegate;
                MyDelegate value2 = (MyDelegate)Delegate.Combine(myDelegate2, value);
                myDelegate = Interlocked.CompareExchange(ref this.MyEvent, value2, myDelegate2);
                if ((object)myDelegate == myDelegate2)
                {
                    break;
                }
            }
        }
        [CompilerGenerated]
        remove // hidden for brevity
    }

    public static void Method1()
    {
        Console.WriteLine("Running Method 1");
    }

    public void M()
    {
        MyEvent += <>O.<0>__Method1 ?? (<>O.<0>__Method1 = new MyDelegate(Method1));
        this.MyEvent();
    }
}

Alright, in this case, we still have a += on the event! Does this mean they're different? A little, but not much! We also have this compiler-generated add on the MyEvent event. This is what gets called for += in the code. If we inspect that add's body, we can see that our old friend Delegate.Combine is what shows up to add the subscriber method (value) onto the current event handler (myDelegate2).

So, all of this is to explain: These two examples are very similar! Your ChatGPT output uses the event keyword to set up a special type of delegate, but fundamentally, the += you use to combine delegates in example 1 works very similarly to the += that adds subscribers to MyEvent in example 2!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment