假设基本接口继承的通用类型列表的Contra / Co Variance

时间:2018-08-28 16:05:07

标签: c# .net generics interface covariance

在.NET中,我具有使用泛型的三级继承,其模式如下:

public interface IManager<TEvent> where TEvent : IEvent 
{ 
    void Foo( TEvent mySpecificEvent ); 
    //... 
}

class Manager<TEvent> : IManager<TEvent> where TEvent : IEvent
class NumericManager<TEvent> : Manager<TEvent> where TEvent : IEvent 
class SpecificNumericManager : NumericManager<ISpecificNumericEvent>

这样,特定的管理者可以处理特定的类型,希望我可以将几种特定的类型保存在列表中,并告诉他们所有人都可以对特定的事件运行Foo(TEvent mySpecificEvent)操作,只要该事件继承自IEvent

当一切正常时,但是我尝试将SpecificNumericManager当作IManager<IEvent>并将其添加到List<IManager<IEvent>>时,出现运行时错误: “无法将类型为SpecificNumericManager的对象转换为类型为IManager<IEvent>的对象”

请注意,我的代码在VB中,但我已经转换为C#,希望获得更多支持,因此,如果我对某些内容有所误解,则可能对此有所解释。

我已经进行了实验,可以将SpecificNumericManager<ISpecificNumericEvent>成功投射到IManager<ISpecificNumericEvent>,但不能直接投射到<Manager<IEvent>>

在发现我可以做些什么来解决它时,通用类型的矛盾和协方差似乎在我的第一步,但我还没有发现窍门。

任何输入表示赞赏。谢谢!

2 个答案:

答案 0 :(得分:1)

@ V0ldek已经详细解释了它发生的原因。基本上,继承关系TDerived : TBase不会传播到T<TDerived> : T<TBase>

一种可能的解决方案是声明一个非通用基类型:

public interface IManager
{
    void Foo(IEvent nonSpecificEvent);
    //... 
}

public interface IManager<TEvent> : IManager
    where TEvent : IEvent
{
    void Foo(TEvent mySpecificEvent);
    //... 
}

这使您可以将列表声明为

List<IManager> _list;

...与直接或间接源自IManager的各种管理人员兼容,无论是否通用。

这将导致Foo的2个重载版本。

通过具有明确实现的定义接口以外的类型访问类型时,可以隐藏成员:

class Manager<TEvent> : IManager<TEvent> where TEvent : IEvent
{
    public void Foo(TEvent mySpecificEvent)
    {
        //...
    }

    // Explicit implementation
    void IManager.Foo(IEvent nonSpecificEvent)
    {
        //...
    }
}

在VB中,可以通过将其声明为私有来完成

Private Sub Foo(nonSpecificEvent As IEvent) Implements IManager.Foo

Public Interface IManager
    Sub Foo(nonSpecificEvent As IEvent)
End Interface


Public Interface IManager(Of TEvent As IEvent)
    Inherits IManager

    Overloads Sub Foo(mySpecificEvent As TEvent)
End Interface


Class Manager(Of TEvent As IEvent)
    Implements IManager(Of TEvent)

    Public Sub Foo(mySpecificEvent As TEvent) Implements IManager(Of TEvent).Foo
    End Sub

    ' Hidden  implementation
    Private Sub Foo(nonSpecificEvent As IEvent) Implements IManager.Foo
    End Sub
End Class

答案 1 :(得分:0)

您需要IManager进行协变。

interface IManager<out TEvent> where TEvent : IEvent

这样,此分配就合法了:

IManager<IEvent> manager = new Manager<IMoreSpecificEvent>();
在最基本的意义上,

协方差指定您不在乎类型参数的派生是否超过此类分配的约束。这意味着,除其他外,协变实现中无处可寻(因此在您的Manager中 您可以在C#here中了解有关协方差的更多信息。

编辑:

对,所以这个方法:

void Foo(TEvent mySpecificEvent);

在这里破坏了我们的小计划。目前,您的Manager并不协变,这是有充分理由的。让我们考虑一个列表:

List<IManager<IEvent>> list;

,并说这是合法的分配(不是,但让我们假装):

IManager<ISpecificNumericEvent> manager = new SpecificNumericManager();

list.Add(manager);

如果我们传递此列表在管理器上执行一些操作会发生什么?好吧,我们所知道的是里面的东西是IManager<IEvent>类型的。这意味着我们应该能够将它 any IEvent馈送到Foo方法中,对吧?

class BaseEvent : IEvent {}

foreach(var manager in list)
{
    IEvent event = new BaseEvent();

    manager.Foo(event);
}

但是由于我们早先已将SpecificNumericManager分配给列表,因此我们试图调用带有签名的方法:

void Foo(TEvent event) where TEvent : ISpecificNumericEvent

带有BaseEvent,但未实现ISpecificNumericEvent。动臂,在火热的爆炸中摧毁了这种类型的系统,猫和狗在一起生活,歇斯底里。

由于上述原因,该接口不能是协变的-C#简单地禁止更早地绕过类型系统,不允许对IManager<IEvent>进行协变分配。出于相同的原因,将事件循环馈给泛型IManager<IEvent>也是一个坏主意,因为根本不知道要知道某个特定的管理器在编译时是否能够处理该特定的事件。您需要考虑该循环真正要实现的目标。通常,当协方差/相反方差规则被破坏时,这是因为您要执行的操作实际上没有任何意义。但是,如果您想对通用IManager<IEvent>列表中的类型做一些疯狂的事情,那么您就不会拥有类型系统了,就必须使用反射来实现您的隐秘目标。