为什么不';接口工作,但抽象类使用泛型类约束

本文关键字:抽象类 泛型类 约束 接口 工作 为什么不 | 更新日期: 2023-09-27 18:00:52

下面的代码显示了一个具有类型约束(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更改为抽象类而不是接口,错误就会消失?

好问题!

失败的原因是,在从方法组委托类型的转换中,您依赖于形式参数逆变,但只有当每个变化类型都是引用类型

为什么可变类型不是"已知的引用类型"?因为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期望堆栈上有一个对堆内存的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+和+反方差/

顺便说一句,如果你试图在Visual Basic中进行这些转换恶作剧,它会很高兴地允许你这样做。VB会做最后一件事;它会检测到类型不匹配,而不是告诉你这件事以便你可以修复它,它会代表你默默地插入一个不同的委托,为你修复类型。一方面,这是一种很好的"说到做到"的功能,在代码中,它看起来应该起作用。另一方面,你要求用"Notify"方法生成一个委托,而你得到的委托绑定到一个完全不同的方法,该方法是"Notice"的代理,这是非常出乎意料的。

在VB中,设计理念更多的是"默默地纠正我的错误,按照我的意思去做"。在C#中,设计理念更多的是"告诉我我的错误,这样我就可以自己决定如何纠正它们"。两者都是合理的哲学;如果你是那种喜欢编译器为你做出正确猜测的人,你可以考虑研究VB。如果你是这样的人,当编译器引起你的注意而不是猜测你的意思时,C#可能对你更好。

用T 替换IMsg

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