c# – 为什么接口不工作,但抽象类与泛型类约束有关?

下面的代码显示了一个类型约束(Pub< T>)的泛型类.该类有一个事件,它可以提高允许我们传递消息给订阅者.约束是消息必须实现IMsg(或者当它是抽象类时从IMsg继承).

酒馆< T>当且仅当对象实现IHandler< IMsg>时,还提供了一种订阅方法,允许对象订阅通知事件.

使用.NET 4,下面的代码显示了baseImplementer.NotifyEventHandler上的错误,指出:
“IHandler< IMsg> .NotifyEventHandler(IMsg)”的任何重载匹配委托’System.Action< T>‘“

问题:(更新订阅方式)

为什么一旦将“IMsg”更改为抽象类而不是接口,错误就会消失?

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 compiles 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);
}

下面的代码不需要重现问题,但显示如何使用上面的代码.显然,IMsg(和派生的Msg)类将定义或实现可以在处理程序中调用的方法.

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?

好问题!

失败的原因是因为您在从方法组转换为委托类型时依赖于形式参数的相反变化,而每个变化类型都被称为引用类型,则只有covariant and contravariant method group conversions才能合法.

为什么变化型不是“已知的参考类型”?因为T上的接口约束也不会将T限制为参考类型.它限制T是实现接口的任何类型,但是结构类型也可以实现接口!

当使约束成为抽象类而不是接口时,编译器知道T必须是引用类型,因为只有引用类型才能扩展用户提供的抽象类.然后,编译器知道方差是安全的并允许它.

我们来看看你的程序的简单版本,看看如果你允许你需要的转换,它会出错:

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?
    }
}

这是非法的,因为你可以说:

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));

好的,现在想想这是做什么的.在调用方面,该操作期望在调用堆栈上放置一个24字节的结构体S,并且期望被调用者处理它.被调用者Handler.Notify正在期待一个四或八个字节的堆内存引用在堆栈上.我们刚刚将堆栈对齐在16到20个字节之间,而第一个或两个结构体将被解释为指向内存的指针,从而导致运行时崩溃.

这就是为什么这是非法的.在处理动作之前,struct需要被装箱,但是没有提供任何框架框架的代码!

有三种方法可以使这项工作.

首先,如果你保证一切都是参考类型,那么这一切都会奏效.您可以使IMsg成为类类型,从而保证任何派生类型都是引用类型,也可以将“类”约束放在程序中的各种“T”上.

第二,你可以一直使用T:

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

现在你不能通过Handler< IMsg>到C< SMsg> .MakeSomeAction – 您只能传递处理程序< SMsg>,使其Notify方法期望将要传递的结构体.

第三,你可以编写拳击的代码:

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

现在编译器看到啊,他不想直接使用handler.Notify.相反,如果需要发生拳击转换,那么中间功能将会照顾它.

合理?

自C#2.0开始,方法组转换给代表在参数类型和它们的返回类型中是一致的.在C#4.0中,我们还对接口上的转换和委托类型添加了协方差和逆变量,这些类型被标记为对方差是安全的.看起来像您在这里做的各种事情,您可能会在界面声明中使用这些注释.有关这个功能的设计因素,请参阅我的长篇系列的必要背景. (从底部开始.)

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

顺便说一句,如果你试图在Visual Basic中提取这些转换shenanigans,它会高兴地允许你. VB将做相当于最后一件事情;它会检测到类型不匹配,而不是告诉您它,以便您可以修复它,它将默认插入一个不同的委托代表您修复您的类型.一方面,这是一个很好的“做我所说的不是我说的”功能,在该代码看起来应该只是工作.另一方面,您希望通过“通知”方法要求委托人,而您退出的代表必须是完全不同的方法,这是“通知”的代理.

在VB中,设计理念更多的是“默默地解决我的错误,做我的意思”的结束.在C#中,设计理念更多的是“告诉我关于我的错误,所以我可以决定如何自己修复”结束.两者都是合理的哲学;如果您是编译器为您做好猜测时喜欢的人,可以考虑使用VB.如果你是一个喜欢它的人,当编译器会引起你的注意力,而不是猜测你的意思,C#可能会更好.

转载注明原文:c# – 为什么接口不工作,但抽象类与泛型类约束有关? - 代码日志