1

I recently found some strange behaviour of delegates. It appears, that casting a delegate to some other (compatible, or even the same) breaks the equality of delegates. Suppose we have some class with method:

public class Foobar {
   public void SomeMethod(object sender, EventArgs e);
}

Now let's make some delegates:

var foo = new Foobar();
var first = new EventHandler(foo.SomeMethod);
var second = new EventHandler(foo.SomeMethod);

Of course, because delegates with the same target, method and invocation list are considered equal, this assertion will pass:

Assert.AreEqual(first, second);

But this assertion won't:

Assert.AreEqual(new EventHandler(first), new EventHandler(second));

However, the next assertion will pass:

Assert.AreEqual(new EventHandler(first), new EventHandler(first));

This is quite awkward behaviour, since both delegates are considered equal. Converting it to a delegate of even the same type in some way breaks its equality. The same will be, we define our own type of delegate:

public delegate MyEventHandler(object sender, EventArgs e);

Delegates can be converted from EventHandler to MyEventHandler, and in reverse direction, however after this conversion, they won't be equal.

This behaviour is very misleading when we want to define an event with explicit add and remove to pass handler to some other object. Therefore both of event definitions below are acting differently:

public event EventHandler MyGoodEvent {
   add {
      myObject.OtherEvent += value;
   }
   remove {
      myObject.OtherEvent -= value;
   }
}

public event EventHandler MyBadEvent {
   add {
      myObject.OtherEvent += new EventHandler(value);
   }
   remove {
      myObject.OtherEvent -= new EventHandler(value);
   }
}

The first one will work fine. The second one will cause memory leaks, because when we connect some method to the event, we won't be able to disconnect:

var foo = new Foobar();
// we can connect
myObject.MyBadEvent += foo.SomeMethod;
// this will not disconnect
myObject.MyBadEvent -= foo.SomeMethod;

This is because, as it was pointed, after conversion (which occurs in event add and remove) delegates are not equal. Delegate which is added is not the same, as which is removed. This can lead to serious, and hard to find memory leaks.

Of course one can say to use only first approach. But it may be impossible in some circumstances, especially when dealing with generics.

Consider the following scenario. Let's suppose, that we have delegate and interface from third party library, which look like this:

public delegate void SomeEventHandler(object sender, SomeEventArgs e);

public interface ISomeInterface {
   event SomeEventHandler MyEvent;
}

We would like to implement that interface. Inner implementation of this will be based on some other third party library, which has a generic class:

public class GenericClass<T> where T : EventArgs
{    
    public event EventHandler<T> SomeEvent;
}

We want this generic class to expose its event to the interface. For example, we can do something like that:

public class MyImplementation : ISomeInterface {

   private GenericClass<SomeEventArgs> impl = new GenericClass<SomeEventArgs>();

   public event SomeEventHandler MyEvent {
      add { impl.SomeEvent += new SomeOtherEventHandler(value); }
      remove { impl.SomeEvent -= new SomeOtherEventHandler(value); }
   }

}

Because class uses generic event handler, and interface uses some other, we have to make a conversion. Of course, this makes an event impossible to disconnect from. The only way is to store a delegate into a variable, connect it, and disconnect when needed. This is however very dirty approach.

Can someone tell, if it is intended to work like this, or it is a bug? How to connect one event handler to compatible one in a clean way with the ability to disconnect it?

Kędrzu
  • 2,265
  • 11
  • 21
  • 1
    I assume by `Assert.IsTrue(new EventHandler(first), new EventHandler(second));` you meant `Assert.AreEqual(new EventHandler(first), new EventHandler(second));` ? – codenheim Sep 27 '14 at 18:29
  • Of course, thank you. – Kędrzu Sep 27 '14 at 18:35
  • Probably related: https://connect.microsoft.com/VisualStudio/feedback/details/1056344/handler-not-removed-from-event-when-routed-through-a-proxy – Henrik Aug 25 '15 at 10:37

1 Answers1

1

This appears to be intended.1 When you say new DelegateType(otherDelegate) you are actually creating a new delegate that points not to the same target and method as otherDelegate does, but that points to otherDelegate as the target and otherDelegate.Invoke(...) as the method. So they are indeed different delegates:

csharp> EventHandler first = (object sender, EventArgs e) => {};
csharp> var second = new EventHandler(first);
csharp> first.Target;
null
csharp> first.Method;
Void <Host>m__0(System.Object, System.EventArgs)
csharp> second.Target;
System.EventHandler
csharp> second.Method;
Void Invoke(System.Object, System.EventArgs)
csharp> second.Target == first;
true

1 Upon examining the C# specification, it is not clear to me if this is technically in violation of the spec. I reproduce here part of §7.5.10.5 from the C# laguange specification 3.03.0:

The run-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:

  • ...
  • If E is a value of a delegate-type:
    • ...
    • The new delegate instance is initialized with the same invocation list as the delegate instance given by E.

Perhaps it is a matter of interpretation whether "initialized with the same invocation list" can be considered satisfied by having one delegate call the other delegate's Invoke() method. I tend to lean towards "no" here. (Jon Skeet leans towards "yes.")


As a workaround, you can use this extension method to convert delegates while retaining their exact invocation list:

public static Delegate ConvertTo(this Delegate self, Type type)
{
    if (type == null) { throw new ArgumentNullException("type"); }
    if (self == null) { return null; }

    return Delegate.Combine(
        self.GetInvocationList()
            .Select(i => Delegate.CreateDelegate(type, i.Target, i.Method))
            .ToArray());
}

(See a demo.)

Community
  • 1
  • 1
cdhowie
  • 133,716
  • 21
  • 261
  • 264
  • Looks like the only reasonable way is to always use EventHandler generic delegates to avoid casting. However customly defined delegates are widely used in Microsoft libraries. – Kędrzu Oct 06 '14 at 14:05
  • It works! I made some modifications to check this against events, and works well. Here with some strong type conversion: http://ideone.com/fBS2PH – Kędrzu Oct 07 '14 at 16:26