The difference between += and Delegate.Combine
Asked Answered
C

1

6

I've built an event system that maintains a dictionary of delegates, adds/removes elements to this dictionary via generic Subscribe/Unsubscribe methods (which each take an Action of type T), and has a Publish method to notify the subscribers when something happens (which also takes an Action of type T). Everything works fine, but I've noticed I cannot use += or -= when adding or removing elements to my dictionary, as the types passed into the methods (Action of T), do not match the types stored in the dictionary (Delegate). The following snippet shows what I can and cannot do.

private readonly Dictionary<Type, Delegate> delegates = new Dictionary<Type, Delegate>();

public void Subscribe<T>(Action<T> del)
{
    if (delegates.ContainsKey(typeof(T)))
    {
        // This doesn't work!
        delegates[typeof(T)] += del as Delegate;
        // This doesn't work!
        delegates[typeof(T)] += del;
        // This is ok
        delegates[typeof(T)] = (Action<T>)delegates[typeof(T)] + del;
        // This is ok
        var newDel = (Action<T>)delegates[typeof(T)] + del;
        delegates[typeof(T)] = newDel;
        // This is ok
        del += (Action<T>)delegates[typeof(T)];
        delegates[typeof(T)] = del;
        // This is ok
        delegates[typeof(T)] = Delegate.Combine(delegates[typeof(T)], del);
    }
    else
    {
        delegates[typeof(T)] = del;
    }
}

I mostly understand Jon Skeet's answer here += operator for Delegate specifically, this part

The binary + operator performs delegate combination when both operands are of some delegate type D. (If the operands have different delegate types, a binding-time error occurs.)

What I don't understand, is this

The compiler turns it into a call to Delegate.Combine. The reverse operation, using - or -=, uses Delegate.Remove.

What exactly is going on when I use += or -=, versus Delegate.Combine? What are the differences between the two, making one implementation valid, and another invalid?

Chemical answered 11/4, 2019 at 8:8 Comment(2)
It should be clear - the compiler has to know what it's working with to do a Delegate.Combine. It doesn't have enough information based on the declared type of delegates to make this decision.Athalie
All the ones that work (except the last) work because you're inserting casts. Giving the compiler more information because you "know better" than it does what the actual type is.Athalie
Z
7

At the end of the answer you linked, it says:

Since System.Delegate is not a delegate type, operator + is not defined for it.

This explains why this does not work (both operands are Delegate):

delegates[typeof(T)] += del as Delegate;

Delegate.Combine works because it is declared like this:

public static Delegate Combine (params Delegate[] delegates);

Note how it accepts parameters of type Delegate, instead of a specific delegate type. And it will throw an ArgumentException if the delegates are not of the same type.

So the compiler not only changes the + operator to Delegate.Combine, it also does some type checking! On the other hand, no compile-time type checking is done if you use Delegate.Combine directly. Delegate.Combine only checks the types at runtime.

All the other lines work because you are casting i.e. telling the compiler what type the delegates are, making both operands of + to be of type Action<T>.

Zachariahzacharias answered 11/4, 2019 at 8:28 Comment(2)
Thanks for this, I think I have a better understanding now. I edited the post slightly to include an additional line that doesn't work, as the cast to Delegate was incorrect/misleading re: my confusion. I think I have a better understanding now of what's going on here, particularly because System.Delegate is not a delegate type, operator + is not defined for it Thank you!Chemical
Note that co- and contravariance of generic delegate types has broken some of the type safety here. As an example, Action<in T> is contravariant ("in") in T, and we can create the following example: Action<ICloneable> act1 = x => x.Clone(); Action<IConvertible> act2 = x => x.GetTypeCode(); Action<string> actStr1 = act1; /* OK, contravariance */ Action<string> actStr2 = act2; /* OK, contravariance */ actStr1("dummy"); /* works */ actStr2("dummy"); /* works */ actStr1 += actStr2; /* does not complain at binding-time, blows up at run-time */Idiophone

© 2022 - 2024 — McMap. All rights reserved.