实例化一个泛型类型?

3

我目前正在开发一个通用集合类,并且想要创建一个方法,该方法从集合中返回一个对象。如果特定的对象不存在于集合中,则应创建该对象,将其添加到集合中,然后返回。

然而,我遇到了一些问题。通用集合的类型表示一个抽象类,我无法实例化它。

这是我的类定义:

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase

这是我正在完成的部分方法:

public T Item(int Id)
{
    CacheItem<T> result = this.Where(t => t.Entity.Id == Id).First();

    if (result == null) //item not yet in cache, load it!
    {
        T entity = new T(Id); //design time exception here: Cannot create an instance of the variable type 'T' because it does not have the new() constraint
        result = new CacheItem<T>(entity);
        this.Add(result);
    }

    return result.Entity;
}

有什么方法可以解决这个问题吗?
编辑:所有从EntityBase派生的类都有一个只读属性Id。
6个回答

7

更新2: 你在评论中说你没有定义一个非泛型的CacheCollection类型,但是你接着又说你有一个Dictionary<Type, CacheCollection>。这两个陈述不能同时成立,所以我猜你指的是CacheCollection<EntityBase>

现在问题来了:如果X<T>类型不是协变的,那么X<Derived>就无法转换为X<Base>。也就是说,在你的情况下,仅仅因为T派生自EntityBase并不意味着CacheCollection<T>派生自CacheCollection<EntityBase>

为了具体说明这一点,考虑List<T>类型。假设你有一个List<string>和一个List<object>string派生自object,但这并不意味着List<string>派生自List<object>;如果是这样的话,那么你就可以写出这样的代码:

var strings = new List<string>();

// If this cast were possible...
var objects = (List<object>)strings;

// ...crap! then you could add a DateTime to a List<string>!
objects.Add(new DateTime(2010, 8, 23));

幸运的是,我认为解决这个问题的方法非常简单。基本上,按照我的最初建议,定义一个非通用的基类,从中派生CacheCollection<T>。更好的做法是使用一个简单的非通用接口。

interface ICacheCollection
{
    EntityBase Item(int id);
}

请看下面更新过的代码,以了解如何在通用类型中实现该接口。
然后对于您的字典,不要使用`Dictionary >`,而是定义为`Dictionary `,您的其余代码应该就能组合起来了。
更新:看来你一直没有告诉我们全部信息!所以您有一个非泛型的CacheCollection基类,其中CacheCollection派生自该基类,是吗?
如果我对您最新评论的理解是正确的,那么我的建议是编写一个类以间接访问您的这个Dictionary。这样,您可以拥有许多CacheCollection实例,而不会牺牲类型安全。
像这样(注意:根据上面的new更新修改的代码)
class GeneralCache
{
    private Dictionary<Type, ICacheCollection> _collections;

    public GeneralCache()
    {
        _collections = new Dictionary<Type, ICacheCollection>();
    }

    public T GetOrAddItem<T>(int id, Func<int, T> factory) where T : EntityBase
    {
        Type t = typeof(T);

        ICacheCollection collection;
        if (!_collections.TryGetValue(t, out collection))
        {
            collection = _collections[t] = new CacheCollection<T>(factory);
        }

        CacheCollection<T> stronglyTyped = (CacheCollection<T>)collection;
        return stronglyTyped.Item(id);
    }
}

这将允许您编写如下代码:

这段代码示例:

var cache = new GeneralCache();

RedEntity red = cache.GetOrAddItem<RedEntity>(1, id => new RedEntity(id));
BlueEntity blue = cache.GetOrAddItem<BlueEntity>(2, id => new BlueEntity(id));

如果 T 派生自 EntityBase 但没有无参构造函数,你最好的选择是指定一个工厂方法,在你的 CacheCollection<T> 构造函数中生成适当参数的 T

像这样 (注意:代码根据上面的新更新进行了修改):

public class CacheCollection<T> : List<CacheItem<T>>, ICacheCollection where T : EntityBase
{
    private Func<int, T> _factory;

    public CacheCollection(Func<int, T> factory)
    {
        _factory = factory;
    }

    // Here you can define the Item method to return a more specific type
    // than is required by the ICacheCollection interface. This is accomplished
    // by defining the interface explicitly below.
    public T Item(int id)
    {
        // Note: use FirstOrDefault, as First will throw an exception
        // if the item does not exist.
        CacheItem<T> result = this.Where(t => t.Entity.Id == id)
            .FirstOrDefault();

        if (result == null) //item not yet in cache, load it!
        {
            T entity = _factory(id);

            // Note: it looks like you forgot to instantiate your result variable
            // in this case.
            result = new CacheItem<T>(entity);

            Add(result);
        }

        return result.Entity;
    }

    // Here you are explicitly implementing the ICacheCollection interface;
    // this effectively hides the interface's signature for this method while
    // exposing another signature with a more specific return type.
    EntityBase ICacheCollection.Item(int id)
    {
        // This calls the public version of the method.
        return Item(id);
    }
}

如果您的项目将拥有独特的ID,我建议您使用Dictionary<int, CacheItem<T>>作为支持存储,而不是List<CacheItem<T>>。这样可以使您的项目查找时间复杂度从O(N)降至O(1)。
此外,我还建议使用私有成员来保存集合本身,而不是直接继承集合。因为继承会暴露您可能想要隐藏的功能,比如AddInsert等。

谢谢,丹。这绝对是正确的方向。这让我想到了另一个问题。在工厂函数中应该放入什么?有没有办法使其也变得通用,或者我需要100个不同的方法来代表可以放入我的集合中的100个不同类别? - Sonny Boy
@Jeroen:我不能说这是最漂亮的解决方案,但它很灵活。假设OP不想为他的类型定义一个无参数构造函数和/或他想让“Id”属性只读。另一种方法是定义一个受保护的抽象“CreateEntity”方法,但这将迫使OP从每个他想要集合的类型中继承“CacheCollection”(不是很吸引人)。我个人发现“new”约束在存在无参数构造函数的情况下很有用,但在需要添加一个仅满足约束的构造函数时则不然。 - Dan Tao
@Sonny Boy:假设你有一个从EntityBase继承并带有一个接受int参数的构造函数的类型CustomEntity。那么你可以通过编写var cache = new CacheCollection<CustomEntity>(id => new CustomEntity(id));来创建这些实体的集合。 - Dan Tao
@Dan Tao - 如果我在运行时不知道我是否有CustomEntity或CustomEntity2,那么有没有一种方法可以构造这段代码的结构?我希望有一个单一的管理对象来存储所有的CacheCollections,并通过我正在使用的EntityBase的System.Type来访问它们。 - Sonny Boy
@Sonny Boy:看一下我的更新答案,看能否对你有所帮助。 - Dan Tao
显示剩余9条评论

0

目前没有任何阻止您(或其他人)声明一个 CacheCollection<T> 实例,其中 T 是某些无法直接创建的东西(例如,如果 T 表示抽象类)。正如错误提示所示,您可以通过将 new() 约束添加到泛型参数来要求 T 可以“被创建”:

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, new()

你仍然会遇到一个问题:无法告诉C#编译器,T将有一个接受类型为int的参数的构造函数。解决这个问题最简单的方法可能是创建T的实例,然后赋值其Id属性(假设EntityBase定义了这样的属性):

T entity = new T { Id = Id };

0

我相信编译器报错已经告诉了你解决方案。你需要添加另一个类型约束,以便它知道你可以创建一个实例。你只需要添加new()即可。

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, new()

但这只允许您使用默认或无参数构造函数创建实例。因此,您需要在EntityBase上指定一个属性,以便允许您设置所需的值,或添加另一个约束来使用接口。

例如:

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, new(), IIdSetter

问题在于,EntityBase的子类不能保证具有您想要的构造函数。
此外,您可以使用反射来查找和调用构造函数:
var constructor = entitType.GetConstructor(new Type[] { typeof(int) });
return (EntityBase)constructor.Invoke(new object[] { id });

但是,除非您跟上所有子类实现的步伐,否则无法保证在运行时实现此目标。


0

两件事情:

// add new() to your where clause:
public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, new()

// change the creation of the object:
...
T entity = new T();
entity.Id = Id;
...

那么 T entity = new T { Id = Id }; 会有帮助吗?(我从未在这种情况下尝试过) - davehauser
1
不。如果“Id”是只读属性,那么陶丹的建议可能是最简单和最安全的解决方案。 - Daniel Pratt

0

在类似的情况下,我所做的是创建一个IId接口。

public interface IId
{
   public Id { get; set; }
}

通过使用部分类,向实体类添加接口声明非常容易:

public partial MyClass : IId { }

你的类定义现在变成了:

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, IId, new()

0
T entity = (T)Activator.CreateInstance(typeof(T), Id);

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