C# 模板参数中的“协方差”

C# 模板参数中的“协方差”,c#,C#,假设我有这两门课 class BaseClass { protected HashSet<BaseClass> container; } class DerivedClass : BaseClass { DerivedClass() { container = new HashSet<DerivedClass>(); } } 然后我收到一个错误:无法转换 因为每个

假设我有这两门课

class BaseClass 
{
    protected HashSet<BaseClass> container;
}


class DerivedClass : BaseClass
{
    DerivedClass() 
    {
        container = new HashSet<DerivedClass>(); 
    }
}                         
然后我收到一个错误:无法转换

因为每个DerivedClass都应该是基类,所以我不太清楚为什么会抛出这个错误,但事实确实如此

基类的目标是在容器上执行各种操作,只有特定的行为绑定到DerivedClass,其中要求容器的类型为HashSet


这个目标通常是如何实现的?

您可以做的是:

class BaseClass<T> where T : BaseClass<T>
{
   protected HashSet<T> container;
}


class DerivedClass : BaseClass<DerivedClass>
{
   DerivedClass() 
  {
      container = new HashSet<DerivedClass>(); 
  }
}

你能做的是:

class BaseClass<T> where T : BaseClass<T>
{
   protected HashSet<T> container;
}


class DerivedClass : BaseClass<DerivedClass>
{
   DerivedClass() 
  {
      container = new HashSet<DerivedClass>(); 
  }
}
每个DevrivedClass都是基类,但不是相反。哈希集不能是协变的,因为它允许写操作。因此,在您的场景中,这是可能的:

class BaseClass 
{
   protected HashSet<BaseClass> container;

   public DoSomething()
   {
       container.Add(new BaseClass());   // not legal if container is really a List<DerivedClass>
   }
}
这看起来有点奇怪,但是对于一个类来说,包含相同类型的对象列表看起来很奇怪,所以在您的实际场景中,这可能是有意义的。

每个DevrivedClass都是基类,但反过来说就不一样了。哈希集不能是协变的,因为它允许写操作。因此,在您的场景中,这是可能的:

class BaseClass 
{
   protected HashSet<BaseClass> container;

   public DoSomething()
   {
       container.Add(new BaseClass());   // not legal if container is really a List<DerivedClass>
   }
}

这看起来有点奇怪,但是对于一个类来说,包含相同类型的对象列表看起来很奇怪,所以在您的实际场景中,它可能是有意义的。

协方差只支持接口。也就是说,容器必须声明为继承IEnumerable的,IEnumerable是协变的

编辑


正如尼科所说,这不会像预期的那样起作用。要阐明为什么这是不可能的,请考虑:HashSet.AddDerivedClassA是一个有效的操作,但HashSet.AddDerivedClassA不是。因此,HashSet不能是HashSet。

协方差仅支持接口。也就是说,容器必须声明为继承IEnumerable的,IEnumerable是协变的

编辑

正如尼科所说,这不会像预期的那样起作用。要阐明为什么这是不可能的,请考虑:HashSet.AddDerivedClassA是一个有效的操作,但HashSet.AddDerivedClassA不是。因此,HashSet不能是HashSet

再扩展一点:基类有许多派生类; 派生类在功能上有很多共性。但是 关键的是,每个派生类的变体只需要1个 信息没有冗余。因此,问题是——如何保持干燥 可能的请注意,本例中的基类将具有 对集装箱进行操作。可能会有一个 FlyWeightDerivedClassFactory在新的 DerivedClass被实例化来解决这个问题,但是 去封装事物

考虑创建一个泛型容器类,该类为基类调用其操作提供接口:

interface IContainer {
    void Operation1();
    void Operation2();
}

class Container<T> : IContainer where T : BaseClass {
    private HashSet<T> _internalContainer;

    ...
}

class BaseClass {
    protected IContainer container;
}

class DerivedClass : BaseClass {
    DerivedClass()  {
        container = new Container<DerivedClass>(); 
    }
}
所有特定于类型的操作都应该是容器内部的,或者是从容器派生的类(如果需要的话),而接口方法是类型无关的

再扩展一点:基类有许多派生类; 派生类在功能上有很多共性。但是 关键的是,每个派生类的变体只需要1个 信息没有冗余。因此,问题是——如何保持干燥 可能的请注意,本例中的基类将具有 对集装箱进行操作。可能会有一个 FlyWeightDerivedClassFactory在新的 DerivedClass被实例化来解决这个问题,但是 去封装事物

考虑创建一个泛型容器类,该类为基类调用其操作提供接口:

interface IContainer {
    void Operation1();
    void Operation2();
}

class Container<T> : IContainer where T : BaseClass {
    private HashSet<T> _internalContainer;

    ...
}

class BaseClass {
    protected IContainer container;
}

class DerivedClass : BaseClass {
    DerivedClass()  {
        container = new Container<DerivedClass>(); 
    }
}

所有特定于类型的操作都应该是容器内部的,或者是从容器派生的类(如果需要的话),而接口方法是类型无关的。

协方差只允许在接口中使用。我建议阅读整个系列。这听起来不像是一个伟大的架构。为什么基类会和它的派生类型混在一起?为什么集合不能是新的HashSet?您的设计不是类型安全的。如果基类中的某个方法试图将基类的任意实例添加到容器中,则会出现类型冲突。@NicoSchertler:每个派生类都必须将容器专用化。这是一个相当混乱的问题。好吧,接受。我仍然会考虑基类中集合的重要性。可能一个IEnumerable就足够了,它是协变的。协变只允许在接口中出现。我建议阅读整个系列。这听起来不像是一个伟大的架构。为什么基类会和它的派生类型混在一起?为什么集合不能是新的HashSet?您的设计不是类型安全的。如果基类中的某个方法试图将基类的任意实例添加到容器中,则会出现类型冲突。@NicoSchertler:每个派生类都必须将容器专用化。这是一个相当混乱的问题。好吧,接受。我仍然会考虑基类中集合的重要性。也许一个IEnumerable就足够了,这就是covari
ant.IEnumerable是协变的,ISet不是因为它允许读写操作IEnumerable是协变的,ISet并不是因为它允许读写操作。这种关系似乎是一个很好的例子,说明什么时候可能有更好的设计不使用继承。这种关系似乎是一个很好的例子,说明什么时候可能有更好的设计不使用继承。