【问题标题】:Instantiating a generic type?实例化泛型类型?
【发布时间】:2011-04-02 18:51:30
【问题描述】:

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

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

这是我的类定义:

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 作为只读属性。

【问题讨论】:

    标签: c# .net generics oop collections


    【解决方案1】:

    更新 2:嗯,你在评论中说你没有定义非泛型 CacheCollection 类型;但是你接着说你有一个Dictionary&lt;Type, CacheCollection&gt;。这些陈述不可能都是真的,所以我猜CacheCollection 你的意思是CacheCollection&lt;EntityBase&gt;

    现在问题来了:如果X&lt;T&gt; 类型为is not covariant,则X&lt;Derived&gt; 不能转换为X&lt;Base&gt;。也就是说,在您的情况下,仅仅因为 T 派生自 EntityBase 并不意味着 CacheCollection&lt;T&gt; 派生自 CacheCollection&lt;EntityBase&gt;

    要具体说明为什么会这样,请考虑List&lt;T&gt; 类型。假设您有一个List&lt;string&gt; 和一个List&lt;object&gt;string 派生自object,但并不意味着List&lt;string&gt; 派生自List&lt;object&gt;;如果是这样,那么你可以有这样的代码:

    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&lt;T&gt; 将从该基类中派生,遵循我最初的建议。更好的是,使用简单的非通用接口。

    interface ICacheCollection
    {
        EntityBase Item(int id);
    }
    

    (查看下面我更新的代码,了解如何在泛型类型中实现此接口)。

    然后对于您的字典,而不是 Dictionary&lt;Type, CacheCollection&lt;EntityBase&gt;&gt;,将其定义为 Dictionary&lt;Type, ICacheCollection&gt;,然后您的其余代码应该放在一起。


    更新:您似乎在隐瞒我们!所以你有一个非泛型的 CacheCollection 基类,CacheCollection&lt;T&gt; 派生自它,对吗?

    如果我对您对此答案的最新评论的理解是正确的,这是我给您的建议。编写一个类来提供对您的Dictionary&lt;Type, CacheCollection&gt; 的间接访问。这样,您可以拥有许多 CacheCollection&lt;T&gt; 实例,而不会牺牲类型安全性。

    类似这样的东西(注意:代码根据上面的更新修改):

    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&lt;T&gt; 中的适当参数生成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&lt;int, CacheItem&lt;T&gt;&gt; 作为您的后备存储而不是 List&lt;CacheItem&lt;T&gt;&gt;,因为它会使您的项目查找 O(1) 而不是 O(N )。

    (我还建议使用私有成员来实现这个类来保存集合本身,而不是直接从集合继承,因为使用继承会暴露您可能想要隐藏的功能,例如AddInsert 等)

    【讨论】:

    • 赞成底部的建议。我不确定我是否喜欢这个解决方案中的工厂。
    • 谢谢,丹。这显然是在正确的轨道上。不过,这让我想到了另一个问题。我应该/应该在 _factory 函数中添加什么?有没有办法使这个通用化或者我需要 100 种不同的方法来表示可以进入我的集合的 100 个不同的类?
    • @Jeroen:我不能说这是最漂亮的解决方案,但它很灵活。假设 OP 不想为其类型定义无参数构造函数,和/或他希望 Id 属性为只读。另一种方法是定义一个受保护的抽象 CreateEntity 方法,但这将迫使 OP 为他想要收集的每种类型从 CacheCollection 继承(不是很吸引人)。我个人认为new 约束在存在无参数构造函数的情况下很有用,但在需要添加一个以满足约束的情况下则不然。
    • @Sonny Boy:假设您有一个派生自EntityBase 的类型CustomEntity,并且有一个接受int 参数的构造函数。然后您可以通过编写 var cache = new CacheCollection&lt;CustomEntity&gt;(id =&gt; new CustomEntity(id)); 来创建这些集合
    • @Dan Tao - 如果在运行时之前我不知道我是否有 CustomEntity 或 CustomEntity2,有没有办法可以构建此代码?我希望有一个管理器对象来存储所有 CacheCollections 并通过我正在使用的 EntityBase 的 System.Type 访问它们。
    【解决方案2】:

    目前没有什么可以阻止您(或其他人)声明CacheCollection&lt;T&gt; 的实例,其中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 };
    

    【讨论】:

      【解决方案3】:

      我相信您收到的编译器错误会告诉您解决方案。您必须添加另一个类型约束,以便它知道您可以创建一个实例。您需要做的就是添加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 });
      

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

      【讨论】:

        【解决方案4】:

        两件事:

        // 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 }; 有帮助吗? (在这种情况下我从未尝试过)
        • 没有。如果Id是只读属性,那么丹涛的建议可能是最简单最安全的解决方案了。
        【解决方案5】:

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

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

        通过使用部分类很容易将接口声明添加到您的实体类中:

        public partial MyClass : IId { }
        

        你的类定义现在变成:

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

        【讨论】:

          【解决方案6】:
          T entity = (T)Activator.CreateInstance(typeof(T), Id);
          

          【讨论】:

            猜你喜欢
            • 2013-10-03
            • 1970-01-01
            • 2013-09-18
            • 1970-01-01
            • 1970-01-01
            • 2018-12-19
            • 1970-01-01
            相关资源
            最近更新 更多