下面的代码显示了一个带有类型约束的泛型类(Pub<T>
)。该类有一个事件,它可以提高我们允许向用户传递消息。约束条件是消息必须实现IMsg
(或者当它是抽象类时从IMsg
继承)。Pub<T>
还提供了一个Subscribe
方法,允许对象订阅notify
事件,前提是且仅当对象实现IHandler<IMsg>
时。
使用.NET 4,下面的代码在baseImplementer.NotifyEventHandler
上显示一个错误,指出:"No overload for 'IHandler<IMsg>.NotifyEventHandler(IMsg)' matches delegate '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
}
}
最佳答案
为什么只要将IMsg
改为抽象类而不是接口,错误就会消失?
好问题!
失败的原因是,在从方法组到委托类型的转换过程中,您依赖于形式参数反变量,但只有当已知每个变化类型都是引用类型时,到委托的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字节的结构,并期望被调用方处理它。被调用方handler.notify期望堆栈上存在对堆内存的4或8字节引用。我们刚刚将堆栈偏移了16到20个字节,结构的第一个或两个字段将被解释为指向内存的指针,从而导致运行时崩溃。
这就是为什么这是非法的。在处理操作之前,需要对结构进行装箱,但是您没有提供任何装箱结构的代码!
有三种方法可以让这个工作。
首先,如果你保证所有的东西都是一个引用类型,那么一切都会解决。您可以将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
——只能传递Handler<SMsg>
,这样它的notify方法就需要传递结构。第三,您可以编写执行装箱的代码:
class Pub<T> where T : IMsg
{
public static Action<T> MakeSomeAction(IHandler<IMsg> handler)
{
return t => handler.Notify(t);
}
}
现在编译器看到了,啊,他不想使用handler。直接通知。相反,如果需要进行装箱转换,则中间函数将处理它。
有道理?
自C 2.0以来,方法组到委托的转换在其参数类型中是逆变的,在其返回类型中是协变的。在C 4.0中,我们还添加了在接口和委托类型上转换的协方差和反方差,这些类型被标记为对方差是安全的。从您在这里所做的各种事情来看,您可能在接口声明中使用这些注释。有关必要的背景,请参阅我关于此功能的设计因素的长系列。(从底部开始。)
http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/
顺便说一句,如果你尝试在visual basic中使用这些转换的恶作剧,它会很高兴地允许你这样做。vb将做与最后一件事相同的事情;它将检测到类型不匹配,而不是告诉您类型不匹配,以便您可以修复它,它将以您的名义静默地插入一个不同的委托,为您修复类型。一方面,这是一种很好的“言出必行”特性,在代码中,它看起来应该正常工作。另一方面,您要求使用“notify”方法生成一个委托是相当意外的,并且您返回的委托绑定到一个完全不同的方法,即“notify”的代理。
在vb中,设计理念更多的是“默默地修正我的错误,做我想做的事”这句话。在C中,设计理念更多的是“告诉我我的错误,这样我就可以自己决定如何解决它们”的结尾。两者都是合理的哲学;如果你是那种喜欢编译器为你做出正确猜测的人,你可以考虑研究vb。如果你是那种喜欢编译器引起你注意而不是猜测你的意思的人,那么C可能对你更好。