为什么接口不起作用,但抽象类可以通过泛型类约束起作用?
下面的代码显示了具有类型约束的泛型类 (Pub
)。该类有一个可以引发的事件,允许我们向订阅者传递消息。限制是消息必须实现 IMsg
(或者当它是抽象类时从 IMsg
继承)。
Pub
还提供了 Subscribe
方法,允许对象订阅 notify
事件,当且仅当对象实现 IHandler< ;IMsg>
。
使用 .NET 4,下面的代码在 baseImplementer.NotifyEventHandler
上显示错误,指出:“'IHandler
问题:(使用更新的 Subscribe 方法)
为什么错误会立即消失我将“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
}
}
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 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);
}
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
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
好问题!
失败的原因是您在从方法组到委托类型的转换中依赖于形式参数逆变,但是协变和逆变方法组转换到委托仅在已知每个变化类型都是引用类型时才合法。
为什么变化类型不“已知为引用类型”?因为对 T 的接口约束并不同时将 T 约束为引用类型。它限制 T 为任何实现接口的类型,但结构类型也可以实现接口!
当您将约束设置为抽象类而不是接口时,编译器就知道 T 必须是引用类型,因为只有引用类型才能扩展用户提供的抽象类。然后编译器知道方差是安全的并允许它。
让我们看一下程序的一个简单得多的版本,看看如果您允许所需的转换,它会如何出错:
这是非法的,因为您可以说:
好吧,现在考虑一下它的作用。在调用方,该操作期望将 24 字节结构 S 放入调用堆栈上,并期望被调用方处理它。被调用方 Handler.Notify 期望堆栈上有一个对堆内存的四字节或八字节引用。我们刚刚将堆栈错位了 16 到 20 个字节,并且该结构的第一个或两个字段将被解释为指向内存的指针,从而导致运行时崩溃。
这就是为什么这是非法的。在处理操作之前需要对结构进行装箱,但是您没有提供任何对结构进行装箱的代码!
有三种方法可以实现这项工作。
首先,如果你保证一切都是引用类型,那么一切都会成功。您可以将 IMsg 设为类类型,从而保证任何派生类型都是引用类型,也可以将“类”约束放在程序中的各个“T”上。
其次,您可以一致地使用 T:
现在您无法将
Handler
传递给C.MakeSomeAction
——您只能传递Handler。 SMsg>
,以便其 Notify 方法期望将传递的结构。第三,你可以编写进行装箱的代码:
现在编译器看到,啊,他不想直接使用handler.Notify。相反,如果需要进行装箱转换,那么中间函数将处理它。
有道理吗?
自 C# 2.0 以来,方法组到委托的转换在其参数类型上是逆变的,在其返回类型上是协变的。在 C# 4.0 中,我们还在接口和委托类型的转换上添加了协变和逆变,这些类型被标记为对于方差是安全的。从您在这里所做的各种事情来看,您似乎可能在接口声明中使用这些注释。请参阅我关于此功能的设计因素的长系列文章,了解必要的背景。 (从底部开始。)
http://blogs.msdn .com/b/ericlippert/archive/tags/covariance+and+contravariance/
顺便说一句,如果您尝试在 Visual Basic 中进行这些类型的转换恶作剧,它会很高兴允许你。 VB 会做与最后一件事相同的事情;它会检测到存在类型不匹配,并且不会告诉您以便您可以修复它,而是会默默地代表您插入一个不同的委托来为您修复类型。一方面,这是一种很好的“做我的意思而不是我说的”功能,在代码中,看起来它应该工作只是工作。另一方面,相当意外的是,您要求通过“Notify”方法创建委托,并且您返回的委托绑定到一个完全不同的方法,该方法是“通知”的代理。
在 VB 中,设计理念更多的是“默默地修正我的错误并按照我的意思去做”。在 C# 中,设计理念更多地是“告诉我我的错误,以便我可以决定如何自己修复它们”。两者都是合理的哲学;如果您是那种喜欢编译器为您做出正确猜测的人,您可以考虑研究 VB。如果您是那种喜欢编译器让您注意问题而不是猜测您的意思的人,那么 C# 可能更适合您。
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:
That's illegal because you could then say:
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:
Now you cannot pass a
Handler<IMsg>
toC<SMsg>.MakeSomeAction
-- you can only pass aHandler<SMsg>
, such that its Notify method expects the struct that will be passed.Third, you can write code that does boxing:
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.
将 IMsg 替换为 T
Replace IMsg with T