2010-08-23 7 views
3

Je travaille actuellement sur une classe de collection générique et j'aimerais créer une méthode qui retourne un objet de la collection. Si l'objet spécifique n'existe pas dans la collection, l'objet doit être créé, ajouté à la collection, puis renvoyé.Instancier un type générique?

Je rencontre cependant quelques problèmes. La collection générique if d'un type qui représente une classe abstraite et j'ai de la difficulté à l'instancier.

Voilà ma définition de la classe:

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

Et voici la méthode complète en partie sur laquelle je travaille:

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; 
} 

Toutes les idées sur la façon de contourner ce problème?

EDIT: Toutes les classes dérivées d'EntityBase ont l'ID en tant que propriété en lecture seule.

Répondre

7

MISE À JOUR 2: Eh bien, vous dites dans un commentaire que vous n'avez pas défini un type non-générique CacheCollection; mais alors vous continuez à dire que vous avez un Dictionary<Type, CacheCollection>. Ces déclarations ne peuvent pas être vraies, donc je devine que par CacheCollection vous voulez dire CacheCollection<EntityBase>.

Maintenant, voici le problème: un X<Derived> ne peut pas être jeté à un X<Base> si le type X<T>is not covariant. C'est, dans votre cas, juste parce que T dérive de EntityBase ne signifie pas que CacheCollection<T> dérive de CacheCollection<EntityBase>.

Pour une illustration concrète de la raison, reportez-vous au type List<T>. Supposons que vous avez un List<string> et un List<object>. string dérive de object, mais il ne s'ensuit pas que List<string> dérive de List<object>; si elle l'a fait, alors vous pourriez avoir le code comme ceci:

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)); 

Heureusement, la façon de contourner cela (à mon avis) est assez simple. Fondamentalement, aller avec ma suggestion originale en définissant une classe de base non générique à partir de laquelle CacheCollection<T> dérivera. Mieux encore, optez pour une interface non générique simple. (Consultez mon code mis à jour ci-dessous pour voir comment implémenter cette interface dans votre type générique).

Ensuite, pour votre dictionnaire, au lieu d'un Dictionary<Type, CacheCollection<EntityBase>>, définissez-le comme un Dictionary<Type, ICacheCollection> et le reste de votre code devrait se réunir.


MISE À JOUR: Il semble que vous interdise de nous! Vous avez donc une classe de base non générique CacheCollection dont dérive CacheCollection<T>, ai-je raison? Si ma compréhension de votre dernier commentaire à cette réponse est correcte, voici mon conseil pour vous. Ecrivez une classe pour fournir un accès indirect à cette Dictionary<Type, CacheCollection>.De cette façon, vous pouvez avoir beaucoup de CacheCollection<T> instances sans sacrifier la sécurité de type.

Quelque chose comme ça (Note: le code modifié en fonction de nouvelle mise à jour ci-dessus):

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); 
    } 
} 

Cela vous permet d'écrire du code comme le suivant:

var cache = new GeneralCache(); 

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

Eh bien, si T dérive de EntityBase mais n'a pas un paramètre sans tructor, votre meilleur pari va être de spécifier une méthode d'usine qui va générer un T pour les paramètres appropriés dans votre constructeur CacheCollection<T>.

Vous aimez cette (Note: le code modifié en fonction de nouvelle mise à jour ci-dessus):

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); 
    } 
} 

Je recommande également, si vos articles vont avoir un ID unique, d'utiliser un Dictionary<int, CacheItem<T>> comme backing store au lieu d'un List<CacheItem<T>> car cela fera que votre objet recherche O (1) au lieu de O (N).

(je recommande également la mise en œuvre de cette classe à l'aide d'un parlementaire pour tenir la collection elle-même plutôt que d'hériter de la collection directement, en utilisant l'héritage expose les fonctionnalités que vous voulez probablement cachés tels que Add, Insert, etc.)

+0

Renvoyé pour les recommandations en bas. Je ne suis pas sûr si j'aime l'usine dans cette solution. –

+0

Merci, Dan. Cela semble définitivement être sur la bonne voie. Cela m'a amené à une autre question, cependant. Que devrais/devrais-je mettre dans la fonction _factory? Y a-t-il un moyen de rendre ce générique aussi ou ai-je besoin de 100 méthodes différentes pour représenter les 100 classes différentes qui pourraient aller dans ma collection? –

+0

@Jeroen: Je ne peux pas dire que c'est la plus jolie solution, mais c'est flexible. Supposons que l'OP ne veuille pas définir un constructeur sans paramètre pour son type, et/ou qu'il souhaite que la propriété 'Id' soit en lecture seule. Une autre approche consisterait à définir une méthode 'CreateEntity' abstraite protégée, mais cela forcerait l'OP à hériter de' CacheCollection' pour chaque type pour lequel il veut avoir une collection (pas très attrayant). Personnellement, je trouve que la contrainte 'new' est utile dans les cas où un constructeur sans paramètre existe, mais pas où il faudrait en ajouter un juste pour satisfaire la contrainte. –

0

Actuellement, rien ne vous empêche (ou quelqu'un d'autre) de déclarer une instance de CacheCollection<T>T est quelque chose qui ne peut pas être créé directement (si T représentait une classe abstraite, par exemple). Comme les conseils d'erreur, vous pouvez exiger que T être « creatable » en ajoutant la contrainte new() au paramètre générique:

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

Vous allez toujours avoir un problème, cependant: Il n'y a aucun moyen de dire C# compiler T aura un constructeur qui accepte un paramètre de type int. Probablement la meilleure façon autour de cette question est de créer une instance de T puis attribuez-lui sa propriété Id (en supposant que EntityBase définit une telle propriété):

T entity = new T { Id = Id }; 
0

Je crois que l'erreur du compilateur que vous obtenez vous dit la solution. Vous devez ajouter une autre contrainte de type afin qu'il sache que vous pouvez créer une instance. Tout ce que vous devez faire est d'ajouter le new()

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

Mais cela ne vous permettra de créer une instance avec la valeur par défaut ou un constructeur sans paramètre. Vous devrez donc spécifier une propriété sur l'EntityBase pour vous permettre de définir les valeurs dont vous avez besoin ou une interface à utiliser en ajoutant une autre contrainte.

Quelque chose comme:

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

Le problème est qu'un enfant de EntityBase ne peut pas être garantie d'avoir le constructeur que vous voulez.

vous pouvez également utiliser la réflexion pour trouver et appeler le constructeur:

var constructor = entitType.GetConstructor(new Type[] { typeof(int) }); 
return (EntityBase)constructor.Invoke(new object[] { id }); 

Mais encore une fois ce ne peut pas être garantie à l'exécution, sauf si vous restez avec vos implémentations de classe des enfants.

0

Deux choses:

// 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; 
... 
+0

'Id' peut être une propriété en lecture seule – max

+0

Serait alors' T entity = new T {Id = Id}; 'aide? (Je n'ai jamais essayé cela dans un tel cas) – davehauser

+1

Non. Si 'Id' est une propriété en lecture seule, la suggestion de Dan Tao est probablement la solution la plus simple et la plus sûre. –

0

Ce que je l'ai fait dans des situations similaires est de créer une interface IId

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

Il est facile d'ajouter la déclaration d'interface à vos classes d'entités par l'utilisation des classes partielles :

public partial MyClass : IId { } 

Votre définition de classe devient maintenant:

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, IId, new() 
0
T entity = (T)Activator.CreateInstance(typeof(T), Id); 
Questions connexes