开发者

Why doesn't an interface work but an abstract class does with a generic class constraint?

The code below shows a generic class with a type constraint (Pub<T>). The class has an event that it can raise allowing us to pass a message to subscribers. The constraint is that the message must implement IMsg (or inherit from IMsg when it's is an abstract class).

Pub<T> also provides a Subscribe method to allow objects to subscribe to the notify event if and only if the object implements IHandler<IMsg>.

Using .NET 4, the code below shows an error on baseImplementer.NotifyEventHandler stating that:

"No overload for 'IHandler<IMsg>.NotifyEventHandler(IMsg)' matches delegate 'System.Action<T>'"

Question: (with updated Subscribe method)

Why does the error go away as soon as I change `IMsg` to an abstract class instead of an interface?

public interface IMsg { }        // Doesn't work
//public abstract class IMsg { } // Does work

public class Msg : IMsg { }

public class Pub<T> where T : IMsg
{
    public event Action<T> notify;

    public void Subscribe(object subscriber)
    {
        // Subscriber subscribes if it implements IHandler of the exact same type as T
        // This always comp开发者_JAVA百科iles and works
        IHandler<T> implementer = subscriber as IHandler<T>;
        if (implementer != null)
            this.notify += implementer.NotifyEventHandler;
        // If subscriber implements IHandler<IMsg> subscribe to notify (even if T is Msg because Msg implements IMsg)
        // This does not compile if IMsg is an interface, only if IMsg is an abstract class
        IHandler<IMsg> baseImplementer = subscriber as IHandler<IMsg>;
        if (baseImplementer != null)
            this.notify += baseImplementer.NotifyEventHandler;
    }
}

public interface IHandler<T> where T : IMsg
{
    void NotifyEventHandler(T data);
}

Code below here is not necessary to reproduce the issue... but shows how the code above might be used. Obviously IMsg (and the derived Msg) classes would define or implement methods that could be called in a handler.

public class SubA : IHandler<Msg>
{
    void IHandler<Msg>.NotifyEventHandler(Msg data) { }
}

public class SubB : IHandler<IMsg>
{
    void IHandler<IMsg>.NotifyEventHandler(IMsg data) { }
}

class MyClass
{
    Pub<Msg> pub = new Pub<Msg>();
    SubA subA = new SubA();
    SubB subB = new SubB();

    public MyClass()
    {
        //Instead of calling...
        this.pub.notify += (this.subA as IHandler<Msg>).NotifyEventHandler;
        this.pub.notify += (this.subB as IHandler<IMsg>).NotifyEventHandler;

        //I want to call...
        this.pub.Subscribe(this.subA);
        this.pub.Subscribe(this.subB);

        //...except that the Subscribe method wont build when IMsg is an interface
    }
}


Why does the error go away as soon as I change IMsg to an abstract class instead of an interface?

Good question!

The reason this fails is because you are relying upon formal parameter contravariance in the conversion from the method group to the delegate type, but covariant and contravariant method group conversions to delegates are only legal when every varying type is known to be a reference type.

Why is the varying type not "known to be a reference type"? Because an interface constraint on T does not also constrain T to be a reference type. It constrains T to be any type that implements the interface, but struct types can implement interfaces too!

When you make the constraint an abstract class instead of an interface then the compiler knows that T has to be a reference type, because only reference types can extend user-supplied abstract classes. The compiler then knows that the variance is safe and allows it.

Let's look at a much simpler version of your program and see how it goes wrong if you allow the conversion you want:

interface IMsg {}
interface IHandler<T> where T : IMsg
{
    public void Notify(T t);
}
class Pub<T> where T : IMsg
{
    public static Action<T> MakeSomeAction(IHandler<IMsg> handler)
    {
        return handler.Notify; // Why is this illegal?
    }
}

That's illegal because you could then say:

struct SMsg : IMsg { public int a, b, c, x, y, z; }
class Handler : IHandler<IMsg> 
{
    public void Notify(IMsg msg)
    {
    }
}
...
Action<SMsg> action = Pub<SMsg>.MakeSomeAction(new Handler());
action(default(SMsg));

OK, now think about what that does. On the caller side, the action is expecting to put a 24 byte struct S on the call stack, and is expecting the callee to process it. The callee, Handler.Notify, is expecting a four or eight byte reference to heap memory to be on the stack. We've just misaligned the stack by between 16 and 20 bytes, and the first field or two of the struct is going to be interpreted as a pointer to memory, crashing the runtime.

That's why this is illegal. The struct needs to be boxed before the action is processed, but nowhere did you supply any code that boxes the struct!

There are three ways to make this work.

First, if you guarantee that everything is a reference type then it all works out. You can either make IMsg a class type, thereby guaranteeing that any derived type is a reference type, or you can put the "class" constraint on the various "T"s in your program.

Second, you can use T consistently:

class Pub<T> where T : IMsg
{
    public static Action<T> MakeSomeAction(IHandler<T> handler) // T, not IMsg
    {
        return handler.Notify; 
    }
}

Now you cannot pass a Handler<IMsg> to C<SMsg>.MakeSomeAction -- you can only pass a Handler<SMsg>, such that its Notify method expects the struct that will be passed.

Third, you can write code that does boxing:

class Pub<T> where T : IMsg
{
    public static Action<T> MakeSomeAction(IHandler<IMsg> handler) 
    {
        return t => handler.Notify(t); 
    }
}

Now the compiler sees, ah, he doesn't want to use handler.Notify directly. Rather, if a boxing conversion needs to happen then the intermediate function will take care of it.

Make sense?

Method group conversions to delegates have been contravariant in their parameter types and covariant in their return types since C# 2.0. In C# 4.0 we also added covariance and contravariance on conversions on interfaces and delegate types that are marked as being safe for variance. It seems like from the sorts of things you are doing here that you could possibly be using these annotations in your interface declarations. See my long series on the design factors of this feature for the necessary background. (Start at the bottom.)

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/

Incidentally, if you try to pull these sorts of conversion shenanigans in Visual Basic, it will cheerfully allow you to. VB will do the equivalent of the last thing; it will detect that there is a type mismatch and rather than telling you about it so that you can fix it, it will silently insert a different delegate on your behalf that fixes up the types for you. On the one hand, this is a nice sort of "do what I mean not what I say" feature, in that code that looks like it ought to work just works. On the other hand, it is rather unexpected that you ask for a delegate to be made out of the method "Notify", and the delegate you get back out is bound to a completely different method that is a proxy for "Notify".

In VB, the design philosophy is more on the "silently fix my mistakes and do what I meant" end of the spectrum. In C# the design philosophy is more on the "tell me about my mistakes so I can decide how to fix them myself" end. Both are reasonable philosophies; if you are the sort of person that likes when the compiler makes good guesses for you, you might consider looking into VB. If you're the sort of person who likes it when the compiler brings problems to your attention rather than making a guess about what you meant, C# might be better for you.


Replace IMsg with T

public interface IMsg { }        // Doesn't work

public class Msg : IMsg { }

public class Pub<T> where T : IMsg
{
    public event Action<T> notify;

    public void Subscribe(object subscriber)
    {
        IHandler<T> implementer = subscriber as IHandler<T>; // here

        if (implementer != null)
        {
            this.notify += implementer.NotifyEventHandler;
        }
    }
}

public interface IHandler<T> where T : IMsg
{
    void NotifyEventHandler(T data);
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜