如何创建一个泛型类,其中包含一个只包含其自身类型或其子类型作为子项的Set?

4
abstract class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }

class Reptile : Animal { }

class AnimalWrapper<T> where T : Animal
{
    public ISet<AnimalWrapper<T>> Children { get; set; }
}

class Program
{
    public static void Main(string[] args)
    {
        var foo = new AnimalWrapper<Mammal>();
        foo.Children = new HashSet<AnimalWrapper<Mammal>>();

        var child = new AnimalWrapper<Dog>();
        foo.Children.Add(child);
    }
}

这段代码显然无法编译通过,因为 foo.Children.Add(child);

我不确定上面的代码是否是最清晰的方式来说明我想做什么,所以我会尝试用简单的英语解释:

我想要一个类,其Children对象在相同泛型类型的ISet中。因此,如果我还有var child = new AnimalWrapper<Reptile>();,则编译时,尝试执行foo.Children.Add(child);将失败,因为Reptile不是Mammal并且没有继承Mammal。然而,即使它是派生的,如上所示,也不能正常工作。

最终,能够说ISet<AnimalWrapper<Animal>> baz = new HashSet<AnimalWrapper<Animal>>();然后将new AnimalWrapper<Mammal>()添加到该集合中,以及将new AnimalWrapper<Reptile>()添加到同一集合中,这将是很好的。它们的子代将具有属性Children,它是一个ISet<AnimalWrapper<T>>,其中它是它自己的类型,如上所述。

有没有办法实现这一点,或者我只是对C#期望过高?该死的,我自己都搞糊涂了。

编辑:好的,所以我差不多想出了一个解决方案,没有使用AnimalWrapper,而是使用基本的IAnimal接口:

interface IAnimal { }

abstract class Animal<T> : IAnimal where T : Animal<T>
{
    public ISet<T> Children { get; set; }
}

class Mammal : Animal<Mammal> { }

class Dog : Mammal { }

class Reptile : Animal<Reptile> { }

class Frog : Reptile { }

class Program
{
    public static void Main(string[] args)
    {
        var animals = new HashSet<IAnimal>(); // any animal can be in this
        var mammal = new Mammal();
        animals.Add(mammal);
        mammal.Children = new HashSet<Mammal>();
        var dog = new Dog();
        mammal.Children.Add(dog); // ok! a dog is a mammal
        dog.Children = new HashSet<Dog>(); // in theory, OK, but compile time error
        // because Dog : Mammal, and Mammal defines Animal<Mammal>, therefore Dog's
        // Children is actually ISet<Mammal>, rather than ISet<Dog> (which is what
        // I want, recursively apply the T in Animal.
        Mammal mammal2 = new Mammal();
        dog.Children.Add(mammal2); // should be verboten, but is allowed for the
        // same reason above.
    }
}

你尝试过将child强制转换为'ATypeImpl'吗? - Daniel Wardin
一旦数据结构构建完成,您希望如何与其进行交互? - dtb
我认为如果您使用更具体的名称,理解起来可能会更容易,也很可能能够看出它的缺陷所在。但我稍后会尝试查看... - Jon Skeet
我希望数据结构(如Children)被限制在其子类型中。例如,AnimalWrapper<Mammal>将强制ISet<T>成为ISet<Mammal>。因此,它只能容纳哺乳动物或狗,不能容纳爬行动物。我唯一做到这一点的方法是通过反射,想知道是否有其他方法。 - johansson
2个回答

2
主要问题在于协变上转型(以及与ISet相反的逆变)。尝试以下方式...
abstract class Animal { }
class Mammal : Animal { }
class Dog : Mammal { }
class Reptile : Animal { }

interface INode<out T> where T : Animal
{
    T MySelf { get; }
    IEnumerable<INode<T>> Children { get; }
}

class Node<T> : INode<T>
    where T : Animal
{
    public Node() { this.Children = new HashSet<INode<T>>(); }
    public T MySelf { get; set; }
    public ISet<INode<T>> Children { get; set; }
    IEnumerable<INode<T>> INode<T>.Children { get { return this.Children; } }
}

class Program
{
    static void Main(string[] args)
    {
        // this is a 'typical' setup - to test compiler 'denial' for the Reptile type...

        Node<Mammal> tree = new Node<Mammal>();
        tree.MySelf = new Mammal();

        var node1 = new Node<Mammal>();
        tree.Children.Add(node1);

        var node2 = new Node<Dog>();
        tree.Children.Add(node2);

        var node3 = new Node<Reptile>();
        // tree.Children.Add(node3); // this fails to compile


        // ...and similar just more 'open' - if you 'collect' animals, all are welcome

        Node<Animal> animals = new Node<Animal>();
        animals.MySelf = new Mammal();

        INode<Mammal> mamals = new Node<Mammal>();
        animals.Children.Add(mamals);

        var dogs = new Node<Dog>();
        animals.Children.Add(dogs);

        INode<Animal> reptiles = new Node<Reptile>();
        animals.Children.Add(reptiles);
    }
}

(查看评论)

这并不意味着它可以在您的实际情况中使用-因为这需要进行一些“设计重构”,以使其能够与更复杂的结构(如果可能)保持兼容。

...只是快速说明,如果需要,稍后我会尝试解释得更详细。


是的,看起来就像是原始代码和另一个答案类似,但使用泛型在接口上添加了“out”以解决协变性问题。这对我来说很有意义,但我仍然不确定它是如何得到执行的。这只是因为ISet<T>不是协变的,因此你被迫转换成实际集合中的任何内容吗? - johansson
我会编辑一些解释——我不确定我完全理解这个问题——但是让我试试——首先,最好是去掉'out',看看会发生什么。或者如果你在INode中使用ISet代替IEnumerable。没有了out(它使INode协变)就不允许你向上转型,即INode<Dog> -> INode<Mammal>。因为,如果没有任何标识,它是不允许的(Dog : Mammal并不意味着很多(这取决于你如何使用它)。所以,我们需要让INode<>的行为与IEnumerable的行为基本相似(它们的定义类似)。为此,我们需要禁止任何Add...。 - NSGaga-mostly-inactive
...接受 <T> 作为 输入参数(逆变性 / in)- 这就是 ISet 的问题所在。幸运的是它也是 IEnumerable - 所以我们可以通过保持 INode 的清洁和 '只读' 并枚举来 '欺骗' 它。然后 Node 负责保持 ISet 和添加子节点等。然而,我们仍然要正确地进行向上转型(请记住 ISet<INode> 必须是这样的(不是 ISet<Node>),这是必要的。因此,简而言之 - 您有两个相反的概念 - ISet/添加('in' param T)和向上转型 - 因此我们必须 '分担' 责任。 - NSGaga-mostly-inactive

1
这是因为当您使用泛型类型参数Mammal实例化AnimalWrapper<T>的实例时,Children成员将是ISet<AnimalWrapper<Mammal>>类型,而不是ISet<AnimalWrapper<Dog>>类型。因此,您无法将AnimalWrapper<Dog>的实例添加到通用集合中。
我看到您可以解决这个问题的一种可能的方法是实现一个接口。
interface IAnimalWrapper { }

class AnimalWrapper<T> : IAnimalWrapper where T : Animal
{
    public ISet<IAnimalWrapper> Children { get; set; }
}

那么您需要更改实例化Children集合的方式...
foo.Children = new HashSet<IAnimalWrapper>();

现在您可以添加不同类型的子元素...
foo.Children.Add(new AnimalWrapper<Mammal>());
foo.Children.Add(new AnimalWrapper<Dog>());
foo.Children.Add(new AnimalWrapper<Reptile>());

那样就可以编译了,但我仍然好奇你为什么真的需要这个泛型类(AnimalWrapper<T>)。我想可能有理由,但也许摆脱那种类型会简化事情(取决于更大的上下文)...
abstract class AnimalWithChildren
{
    public ISet<AnimalWithChildren> Children { get; set; }
}
class Mammal : AnimalWithChildren { }
class Dog : Mammal { }
class Reptile : AnimalWithChildren { }

换句话说,只需依靠 ISet<T> 即可提供类型...

var foo = new Mammal();
foo.Children = new HashSet<AnimalWithChildren>();
foo.Children.Add(new Mammal());
foo.Children.Add(new Dog());
foo.Children.Add(new Reptile());

没错,但如果我写IMammalWrapper继承自IAnimalWrapper,同样的问题也会出现。基本上,我想限制包装器中包含的ISet仅限于定义的类型。 - johansson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接