Sco*_*ott 12 c# generics abstract-class interface constraints
下面的代码显示了一个带有类型约束(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>'"
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
    }
}
Eri*_*ert 18
一旦我更改
IMsg为抽象类而不是接口,为什么错误会消失?
好问题!
这种失败的原因是因为您在从方法组到委托类型的转换中依赖于形式参数逆转,但协变和逆变方法组转换到委托只有在已知每种变化类型都是引用类型时才合法.
为什么变化类型不是"已知为参考类型"?因为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个字节,并且第一个或第二个结构字段将被解释为指向内存的指针,从而导致运行时崩溃.
这就是为什么这是非法的.在处理操作之前,需要对结构进行装箱,但是您没有提供任何包含结构的代码!
有三种方法可以完成这项工作.
首先,如果你保证一切都是参考类型,那么一切都会成功.您可以将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.Notify.相反,如果需要进行拳击转换,那么中间函数将处理它.
合理?
自C#2.0以来,方法组对委托的转换在其参数类型和返回类型的协变方面是逆向的.在C#4.0中,我们还在接口和委托类型的转换上添加了协方差和逆变,这些类型被标记为对方差是安全的.从你在这里做的各种事情来看,你可能会在界面声明中使用这些注释.有关必要背景,请参阅我关于此功能的设计因素的长篇系列.(从底部开始.)
http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/
顺便说一句,如果你试图在Visual Basic中提取这些转换恶作剧,它会愉快地允许你.VB会做相当于最后的事情; 它会检测到类型不匹配而不是告诉您它以便您可以修复它,它会默默地代表您插入一个不同的代理,为您修复类型.一方面,这是一个很好的"做我的意思不是我说的"功能,在代码看起来它应该工作正常.另一方面,你要求委托是由"通知"方法制作的,而你要退回的代表绑定的是一个完全不同的方法,它是"通知"的代理,这是相当意想不到的.
在VB中,设计理念更多地是关于"默默地修复我的错误并做我的意思"的结尾.在C#中,设计理念更多的是"告诉我我的错误,以便我自己决定如何解决它".两者都是合理的哲学; 如果你是那种在编译器为你做好猜测时喜欢的人,你可以考虑研究一下VB.如果你是那种喜欢它的人,当编译器给你注意问题而不是猜测你的意思时,C#可能对你更好.