C# 泛型继承问题

12
我希望将从一个类派生出的不同类型的对象通过泛型添加到基础类型的List中。但是我遇到了编译错误。
Error   2   Argument 1: cannot convert from 'ConsoleApplication1.Stable' to 'ConsoleApplication1.ShelterBase<ConsoleApplication1.AnimalBase>'   C:\Users\ysn\Desktop\ConsoleApplication1\ConsoleApplication1\Program.cs 43  26  ConsoleApplication1

我看不出问题在哪里,你能提供其他解决方案吗?

abstract class AnimalBase { public int SomeCommonProperty;}

abstract class ShelterBase<T> where T : AnimalBase
{
    public abstract List<T> GetAnimals();
    public abstract void FeedAnimals(List<T> animals);
}


class Horse : AnimalBase { }

class Stable : ShelterBase<Horse>
{
    public override List<Horse> GetAnimals()
    {
        return new List<Horse>();
    }

    public override void FeedAnimals(List<Horse> animals)
    {
        // feed them
    }
}


class Duck : AnimalBase { }

class HenHouse : ShelterBase<Duck>
{
    public override List<Duck> GetAnimals()
    {
        return new List<Duck>();
    }

    public override void FeedAnimals(List<Duck> animals)
    {
        // feed them
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<ShelterBase<AnimalBase>> shelters = new List<ShelterBase<AnimalBase>>();

        ///////////////////////////// following two lines do not compile
        shelters.Add(new Stable()); 
        shelters.Add(new HenHouse());
        /////////////////////////////

        foreach (var shelter in shelters)
        {
            var animals = shelter.GetAnimals();
            // do sth with 'animals' collection
        }
    }
}

你使用的是哪个版本的 .Net?如果是 .Net 4,你需要研究协变性。 - Russ Clarke
5个回答

19
你可以使用逆变性,但是只有在将抽象类更改为接口并将GetAnimals的返回类型更改为IEnumerable<T>时才能使用,因为List<T>不支持此功能。

可行代码:

abstract class AnimalBase { public int SomeCommonProperty;}

interface IShelterBase<out T> where T : AnimalBase
{
    IEnumerable<T> GetAnimals();
}

class Horse : AnimalBase { }

class Stable : IShelterBase<Horse>
{
    public IEnumerable<Horse> GetAnimals()
    {
        return new List<Horse>();
    }
}

class Duck : AnimalBase { }

class HenHouse : IShelterBase<Duck>
{
    public IEnumerable<Duck> GetAnimals()
    {
        return new List<Duck>();
    }
}

void Main()
{
    List<IShelterBase<AnimalBase>> shelters = new List<IShelterBase<AnimalBase>>();

    shelters.Add(new Stable());
    shelters.Add(new HenHouse());

    foreach (var shelter in shelters)
    {
        var animals = shelter.GetAnimals();
        // do something with 'animals' collection
    }
}

嗨@Daniel,感谢您的回答。这很好用,但我现在有一个新问题。当我将方法 void FeedAnimals(IEnumerable<T> animals); 添加到接口**IShelterBase**时,它会出现编译错误: 无效的方差:类型参数'T'必须在'IShelterBase<T>.FeedAnimals(IEnumerable<T>)'上逆变有效。'T'是协变的。 - Yasin Kilicdere
1
是的,这就是为什么列表也不支持它的原因。一旦T成为输入和输出参数,它就不再起作用了。请参阅其他答案中链接的有关该主题的页面。 - Daniel Hilgarth

3
您可以使用协变性和逆变性来实现此操作,但是您的ShelterBase类需要从接口派生,因为只有接口可以是协变或逆变的。您的列表将需要是List<IShelterBase<T>>,这样就可以工作了。 更多信息请参见此处

2
为了解决这个问题,您实际上不需要协变性。当您使用动物列表时,仍然通过IShelterBase 接口获得AnimalBase。最好直接通过基类公开AnimalBase列表。
一个更好、更清晰的设计是使GetAnimals返回AnimalBase列表,并在每个收容所类中创建重载以返回特定动物的列表。
abstract class ShelterBase<T> where T : AnimalBase
{
    public List<AnimalBase> GetAnimals(){return new List<AnimalBase>();}
}

class Stable : ShelterBase<Horse>
{
    public List<Horse> GetHorses(){return new List<Horse>();}
}

这种设计的另一个问题是通过最派生的类型来公开集合 - 即List,而不是IEnumerableIList。既然你费了心思创建了一个抽象动物集合的类,你应该通过禁止直接插入/删除操作来保护内部集合。一旦你这样做了,事情就变得容易一些了。例如,这是我解决这个问题的方法。

abstract class ShelterBase<T> where T : AnimalBase
{
    protected List<T> _Animals;
    public AnimalBase() {
       _Animals = CreateAnimalCollection();
    }

    protected abstract List<T> CreateAnimalCollection();
    public IEnumerable<AnimalBase> GetAnimals(){return _Animals.Cast<AnimalBase>();}

    //Add remove operations go here
    public void Add(T animal){_Animals.Add(animal);}
    public void Remove(T animal){_Animals.Remove(animal);}

}

class Stable : ShelterBase<Horse>
{
    protected override List<Horse> CreateAnimalCollection(){return new List<Horse>();}

    public IEnumerable<Horse> GetHorses(){return _Animals;}
}

您会注意到,动物内部集合从未公开为可变列表。这是很好的,因为它允许您更严格地控制其内容。基础收容所中的AddRemove方法在此示例中有点牵强,因为它们与直接访问集合没有任何额外的区别,但您可以在那里添加逻辑 - 例如,在添加时检查最大收容所大小或在删除之前检查动物年龄。


1

不,这不是协方差的事情。 - Cheng Chen
本文讨论了泛型中的协变和逆变。 - Russ Clarke

0
我刚遇到了一个类似的问题。 我所有的实体都来自于一个基类,我创建了一个方法来返回一个连接的id列表。因此,我使用了泛型来创建这个方法,但是出现了一些转换错误。我设法将泛型T转换为Object,然后再将Object转换为BaseClass,并加入了一些验证以防万一。
//Needs it generic to use in a lot of derived classes
private String ConcatIds<T>(List<T> listObj)
{
    String ids = String.Empty;

    foreach (T obj in listObj)
    {
        BaseEntity be = CastBack(obj);

        if (ids.Count() > 0)
            ids = ids + String.Format(", {0}", be.Id);
        else
            ids = be.Id.ToString();
    }

    return ids;
}

//I'll probably move it to the Base Class itself
private BaseEntity CastBack<T>(T genericObj)
{
    Type type = typeof(T);

    if (type.BaseType == typeof(BaseEntity))
    {
        Object _obj = (Object)genericObj;
        return (BaseEntity)_obj;
    }
    else
    {
        throw new InvalidOperationException(String.Format("Cannot convert {0} to BaseEntity", type.ToString()));
    }
}

用法:

public class BaseEntity
{
   public Int32 Id {get; set;}
}

public class AnyDerivedClass : BaseEntity
{
  // Lorem Ipsum
}

private void DoAnything(List<AnyDerivedClass> myList)
{
    String ids = this.ConcatIds<AnyDerivedClass>(myList);
}

编辑:过了一段时间后,我需要创建更多的层级,并且有时需要向父级或祖父级转换。因此,我让我的CastBack方法更加通用。

private B CastBack<T,B>(T genericObj)
{
    Type type = typeof(T);

    if (type.BaseType == typeof(B))
    {
        Object _obj = (Object)genericObj;
        return (B)_obj;
    }
    else
    {
        throw new InvalidOperationException(String.Format("Cannot cast back {0}", type.ToString()));
    }
}

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