1

I have written a very simple console test application in C# (attached below) with an Event and an Event handler. The Event handler is registered and unregistered using several ways.

As far as I've understood, the following two expressions result in the same:

Event1 += handler;
Event1 += new TestEventHandler(handler);

Now I principally expected the same for these two expressions:

Event1 -= handler;
Event1 -= new TestEventHandler(handler);

I relied on the handler being unregistered in both cases. But it seems that there is a difference: It's only different, when I pass the event handler as parameter to a method, then the handler gets not unregistered for the case when the code in the method is implemented using -= new TestEventHandler(handler). However, it works if the code is using -= handler directly.

I don't understand it. Why is this?

Now follows the whole code (you can copy and paste it directly to Visual Studio), and below you find the output that I got:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EventDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Test test = new Test();
            test.Run();
            Console.ReadLine();
        }
    }

    class TestEventArgs : System.EventArgs
    {
        public TestEventArgs() { }
        public TestEventArgs(int no) { No = no; }
        public int No;
    }

    delegate void TestEventHandler(TestEventArgs e);

    public class Test
    {
        public Test()
        {
        }

        void TestHandler(TestEventArgs e)
        {
            Console.WriteLine("No={0}", e.No);
        }

        event TestEventHandler Event1;

        public void Run()
        {
            Console.WriteLine("START");
            Event1 += TestHandler;
            if (Event1 != null)
                Event1(new TestEventArgs(1)); //1
            Event1 -= TestHandler; // Uses: Event1 -= TestHandler;
            if (Event1 != null)
                Event1(new TestEventArgs(2)); //2
            Console.WriteLine("---------- OK. (Event1 == null) is {0}.", Event1 == null);

            Console.WriteLine("START");
            Event1 += new TestEventHandler(TestHandler);
            if (Event1 != null)
                Event1(new TestEventArgs(3)); //3
            Event1 -= new TestEventHandler(TestHandler); // Uses: Event1 -= new TestEventHandler(TestHandler);
            if (Event1 != null)
                Event1(new TestEventArgs(4)); //4
            Console.WriteLine("---------- OK. (Event1 == null) is {0}.", Event1 == null);

            Console.WriteLine("START using Register1/Unregister1");
            RegisterHandler1(TestHandler);
            if (Event1 != null)
                Event1(new TestEventArgs(5)); //5
            UnregisterHandler1(TestHandler); // Uses: Event1 -= TestHandler; where TestHandler is used directly.
            if (Event1 != null)
                Event1(new TestEventArgs(6)); //6
            Console.WriteLine("---------- OK. (Event1 == null) is {0}.", Event1 == null);

            Console.WriteLine("START using Register2/Unregister2");
            RegisterHandler2(TestHandler);
            if (Event1 != null)
                Event1(new TestEventArgs(7)); //7
            UnregisterHandler2(TestHandler); // Uses: Event1 -= new TestEventHandler(handler); where handler was passed as parameter
            if (Event1 != null)
                Event1(new TestEventArgs(8)); //8
            Console.WriteLine("---------- Not OK. (Event1 == null) is {0}.", Event1 == null);
            Console.WriteLine("           I expected that number 8 should not occur, but it does.");

            Console.WriteLine("END.");
        }

        private void RegisterHandler1(TestEventHandler handler)
        {
            Event1 += handler;
        }

        private void UnregisterHandler1(TestEventHandler handler)
        {
            Event1 -= handler;
        }

        private void RegisterHandler2(TestEventHandler handler)
        {
            Event1 += new TestEventHandler(handler);
        }

        private void UnregisterHandler2(TestEventHandler handler)
        {
            Event1 -= new TestEventHandler(handler);
        }
    }

}

the output:

START
No=1
---------- OK. (Event1 == null) is True.
START
No=3
---------- OK. (Event1 == null) is True.
START using Register1/Unregister1
No=5
---------- OK. (Event1 == null) is True.
START using Register2/Unregister2
No=7
No=8
---------- Not OK. (Event1 == null) is False.
           I expected that number 8 should not occur, but it does.
END.
  • Painful question. But yes, scrap the idea that `new DelegateType(methodgroup)` is equivalent to `new DelegateType(DelegateType obj)`. The latter creates a delegate object that points to a dynamically generated stub that invokes `obj`. In effect you invoke twice. The event doesn't get unsubscribed because the addresses of those stubs don't match. I *think* they didn't provide the logical operation because it was too expensive to implement, hard to nail that down. – Hans Passant Dec 11 '14 at 17:29

2 Answers2

0

So it's important to realize what a delegate actually is. Under the hood a delegate is a pointer to a method and an object reference. When you write the code new TestEventHandler(TestHandler) you're creating a delegate that has a pointer to TestHandler and uses this as the object reference.

When you write new TestEventHandler(handler) You're creating a delegate whose method is to invoke handler and who's object is handler.

The Equals method for delegates compares the method pointer and object references that they each store for equality.

We can now write a shorter test cases to examine this issue:

var one = new TestEventHandler(TestHandler);
var two = new TestEventHandler(TestHandler);
var three = new TestEventHandler(one);

Console.WriteLine(object.ReferenceEquals(one, two));
Console.WriteLine(one.Equals(two));
Console.WriteLine(one.Equals(three));

This will print:

false
true
false

one and two both are different object references, given that we new-ed both up, but since they both wrap the same method pointer and object references they are "equal". one and three on the other hand wrap different method pointer and object references, so they're "not equal". Since they're not equal, unsubscribing using three won't remove one from an event.

Servy
  • 202,030
  • 26
  • 332
  • 449
-1

Event1 -= new TestEventHandler(handler) creates a new Instance of a TestEventHandler. It might be functionally identical to the TestEventHandler you created when you first attached the event, but it's a different object in memory, and won't have the same reference.

Skyl3lazer
  • 368
  • 2
  • 10
  • He's creating new delegate references in *every single* one of his tests, not just the last one. – Servy Dec 11 '14 at 15:47
  • That doesn't change my answer. When he tries to run the line of code I mentioned, it's creating a new instance of a TestEventHandler then trying to remove the new instance from the event, I dont know why you downvoted a correct answer :) – Skyl3lazer Dec 11 '14 at 15:48
  • 1
    Your answer *isn't* correct. Delegates don't use the reference to themselves for idenity; they override it to perform a value based comparison. Additionally, as I said, every single delegate he's using to remove the handler has a different reference than the delegate used to add it. The last example is no different in that regard. If this answer were true, *none* of the 4 handlers would be removed, instead of 3 out of 4 being removed. – Servy Dec 11 '14 at 15:51
  • Look at the output of the code. He uses the passed handler in UnregisterHandler2, which passes by value and not by reference, so new TestEventHandler(handler) in that same method creates a TestEventHandler which acts the same as previously but has a different reference, so when he's trying to remove the event handler it's trying to remove a nonexistant handler (because it was never added). In essence he's calling theTestEventHandler constructor to be removed from the eventhandler. see: http://stackoverflow.com/questions/292820/how-to-correctly-unregister-an-event-handler – Skyl3lazer Dec 11 '14 at 15:58
  • You're correct that the two `TestEventHandler` instances will have different references. You're wrong that that is why the code behaves as it does. Delegates override `object.Equals` to perform a value based comparison to determine equality. The fact that there are two different references for the delegates has nothing to do with why they are "not equal". As I said, every single one of the four cases here involves unsubscribing using a delegate with a different reference than was used to subscribe; **every single one**, yet only the last one doesn't work. – Servy Dec 11 '14 at 16:03