为什么接口不起作用,而抽象类却使用generics类约束?

下面的代码显示了一个带有类型约束的通用类( Pub )。 该类有一个可以引发的事件,允许我们向订阅者传递消息。 约束是消息必须实现IMsg (或者当它是抽象类时从IMsginheritance)。

Pub还提供了一个Subscribe方法,当且仅当对象实现了IHandler ,才允许对象订阅notify事件。

使用.NET 4,下面的代码显示baseImplementer.NotifyEventHandler上的错误,指出:
"No overload for 'IHandler.NotifyEventHandler(IMsg)' matches delegate 'System.Action'"

问题:(使用更新的订阅方法)

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

 public interface IMsg { } // Doesn't work //public abstract class IMsg { } // Does work public class Msg : IMsg { } public class Pub where T : IMsg { public event Action 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 implementer = subscriber as IHandler; if (implementer != null) this.notify += implementer.NotifyEventHandler; // If subscriber implements IHandler 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 baseImplementer = subscriber as IHandler; if (baseImplementer != null) this.notify += baseImplementer.NotifyEventHandler; } } public interface IHandler where T : IMsg { void NotifyEventHandler(T data); } 

此处的代码不是重现问题所必需的……但显示了如何使用上面的代码。 显然, IMsg (和派生的Msg )类将定义或实现可在处理程序中调用的方法。

 public class SubA : IHandler { void IHandler.NotifyEventHandler(Msg data) { } } public class SubB : IHandler { void IHandler.NotifyEventHandler(IMsg data) { } } class MyClass { Pub pub = new Pub(); SubA subA = new SubA(); SubB subB = new SubB(); public MyClass() { //Instead of calling... this.pub.notify += (this.subA as IHandler).NotifyEventHandler; this.pub.notify += (this.subB as IHandler).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 } } 

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

好问题!

这种失败的原因是因为您在从方法组委托类型的转换中依赖于forms参数逆转 ,但协变和逆变方法组转换为委托仅在已知每种变化类型为引用类型时才合法。

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

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

让我们看一个更简单的程序版本,如果你允许你想要的转换,看看它是怎么出错的:

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

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

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

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

这就是为什么这是非法的。 在处理动作之前,需要对结构进行装箱,但是你没有提供任何包装结构的代码!

有三种方法可以完成这项工作。

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

其次,您可以始终如一地使用T:

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

现在你不能将Handler传递给C.MakeSomeAction – 你只能传递一个Handler ,这样它的Notify方法就会需要传递的结构。

第三,你可以编写做装箱的代码:

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

现在编译器看到啊,他不想直接使用handler.Notify。 相反,如果需要进行拳击转换,那么中间函数将处理它。

合理?

自C#2.0以来,方法组对委托的转换在其参数类型和返回类型中的协变方面是逆向​​的。 在C#4.0中,我们还对接口和委托类型的转换添加了协方差和逆变,这些类型被标记为对方差是安全的。 从你在这里做的各种事情看来,你可能在界面声明中使用这些注释。 有关必要背景,请参阅我关于此function的设计因素的长篇系列。 (从底部开始。)

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

顺便说一句,如果你试图在Visual Basic中提取这些转换恶作剧,它会愉快地允许你。 VB会做相当于最后的事情; 它会检测到类型不匹配而不是告诉您它以便您可以修复它,它会默默地代表您插入一个不同的代理,为您修复类型。 一方面,这是一个很好的“做我的意思不是我说的”function,在代码看起来它应该工作正常。 另一方面,你要求一个委托是由“通知”方法制作的,并且你回来的委托绑定了一个完全不同的方法 ,它是“通知”的代理,这是相当出乎意料的。

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

用T替换IMsg

 public interface IMsg { } // Doesn't work public class Msg : IMsg { } public class Pub where T : IMsg { public event Action notify; public void Subscribe(object subscriber) { IHandler implementer = subscriber as IHandler; // here if (implementer != null) { this.notify += implementer.NotifyEventHandler; } } } public interface IHandler where T : IMsg { void NotifyEventHandler(T data); }