views:

183

answers:

6

I'm currently working on a generic collection class and I'd like to create a method which returns an object from the collection. If the specific object does not exist in the collection then the object should be created, added to the collection, and then returned.

I'm accounting a few problems though. The generic collection if of a type which represents an abstract class and I'm having trouble instantiating that.

Here's my class definition:

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

And here's the partially complete method on which I am working:

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

Any ideas on how to get around this?

EDIT: All classes derived from EntityBase have Id as a read-only property.

+6  A: 

UPDATE 2: Well, you say in a comment that you have not defined a non-generic CacheCollection type; but then you go on to say that you have a Dictionary<Type, CacheCollection>. These statements cannot both be true, so I am guessing that by CacheCollection you mean CacheCollection<EntityBase>.

Now here's the problem: a X<Derived> cannot be cast to a X<Base> if the X<T> type is not covariant. That is, in your case, just because T derives from EntityBase does not mean that CacheCollection<T> derives from CacheCollection<EntityBase>.

For a concrete illustration of why this is, consider the List<T> type. Say you have a List<string> and a List<object>. string derives from object, but it does not follow that List<string> derives from List<object>; if it did, then you could have code like this:

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

Fortunately, the way around this (in my view) is pretty straightforward. Basically, go with my original suggestion by defining a non-generic base class from which CacheCollection<T> will derive. Better yet, go with a simple non-generic interface.

interface ICacheCollection
{
    EntityBase Item(int id);
}

(Take a look at my updated code below to see how you can implement this interface in your generic type).

Then for your dictionary, instead of a Dictionary<Type, CacheCollection<EntityBase>>, define it as a Dictionary<Type, ICacheCollection> and the rest of your code should come together.


UPDATE: It seems that you were withholding from us! So you have a non-generic CacheCollection base class from which CacheCollection<T> derives, am I right?

If my understanding of your latest comment to this answer is correct, here's my advice to you. Write a class to provide indirect access to this Dictionary<Type, CacheCollection> of yours. This way you can have many CacheCollection<T> instances without sacrificing type safety.

Something like this (note: code modified based on new update above):

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

This would allow you to write code like the following:

var cache = new GeneralCache();

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

Well, if T derives from EntityBase but does not have a parameterless constructor, your best bet is going to be to specify a factory method that will generate a T for the appropriate parameters in your CacheCollection<T> constructor.

Like this (note: code modified based on new update above):

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

I would also recommend, if your items are going to have unique IDs, to use a Dictionary<int, CacheItem<T>> as your backing store instead of a List<CacheItem<T>> as it will make your item lookup O(1) instead of O(N).

(I would also recommend implementing this class using a private member to hold the collection itself rather than inheriting from the collection directly, as using inheritance exposes functionality you probably want hidden such as Add, Insert, etc.)

Dan Tao
Upvoted for the recommendations at the bottom. I'm not sure if I like the factory in this solution.
Jeroen Huinink
Thanks, Dan. This definitely seems to be on the right track.It has led me to another question, though. What would/should I put into the _factory function? Is there a way to make that generic as well or do I need 100 different methods to represent the 100 different classes that could go into my collection?
Sonny Boy
@Jeroen: I can't say it's the prettiest solution, but it's flexible. Suppose the OP doesn't want to define a parameterless constructor for his type, and/or he wants the `Id` property to be read-only. Another approach would be to define a protected abstract `CreateEntity` method, but that would force the OP to inherit from `CacheCollection` for every type he wants to have a collection for (not very appealing). I personally find the `new` constraint to be useful in cases where a parameterless constructor exists, but not where it would require adding one just to satisfy the constraint.
Dan Tao
@Sonny Boy: Say you have a type `CustomEntity` that derives from `EntityBase` and has a constructor that takes an `int` argument. Then you could create a collection of these simply by writing `var cache = new CacheCollection<CustomEntity>(id => new CustomEntity(id));`
Dan Tao
@Dan Tao - Is there a way I can structure this code if I don't know if I have a CustomEntity or CustomEntity2 until runtime? I'm hoping to have a single manager object to store all the CacheCollections and access them by the System.Type of EntityBase I'm working with.
Sonny Boy
@Sonny Boy: var cache = new CacheCollection<MyClass>(i => new MyClass(i));will instantiate a CacheCollection for MyClass, assuming MyClass has a constructor that takes an Id. Any other method that creates a MyClass from an Id will work as well.
Jeroen Huinink
@Dan Tao: maybe the EntityBase class can specify a CreateEntity method that can be used to create a new entity. No need to add it to the CacheCollection if you can add it to EntityBase.
Jeroen Huinink
@Jeroen: That's true, but it's a design compromise. You'll still need to declare public parameterless constructors for your derived types for `EntityBase` to be able to instantiate them. That is unless you're happy with calling `Activator.CreateInstance` to invoke private constructors. But if that's the case then you could've done this in `CacheCollection<T>` too (one advantage I could see, admittedly, would be that you could keep the `set` method for the `Id` property `protected` this way).
Dan Tao
@All - Sorry, guys. I'm feeling kind of stupid for continuing on about this, but I'm still having trouble getting this factory solution to work.I have a series of CacheCollections within another Dictionary<Type, CacheCollection>. I'm not sure how to create a Func<int, T> to pass into the CacheCollection constructor without a massive switch statement representing each possible Type. Is there a way I can use my System.Type within Func<int, T>?
Sonny Boy
@Sonny Boy: Take a look at my updated answer and see if it helps you at all.
Dan Tao
@Dan - I don't have a non-Generic CacheCollection, but I do have a CacheManager with a Dictionary<Type, CacheCollection> within it. I've been able to adapt most of your updated code to my purpose and am down to one last exception. `_collections[t] = new CacheCollection<T>(factory);` is "Cannot implicitly convert type CacheCollection<T> to CacheCollection<EntityBase>".
Sonny Boy
@SonnyBoy: You definitely have *some* kind of non-`CacheCollection` type -- otherwise, what is in your dictionary? It seems from your comment that you've defined a `CacheCollection` type that inherits from `CacheCollection<EntityBase>`, is that right? If so, this is unfortunately not the right approach, as your `CacheCollection<T>` cannot be considered covariant (see [here](http://msdn.microsoft.com/en-us/library/dd799517.aspx) for more details). I will post an alternative method momentarily.
Dan Tao
PERFECT! Thank you so very much for your help with this one.
Sonny Boy
@Sonny Boy: Nice! Glad to help out.
Dan Tao
A: 

Presently there is nothing preventing you (or someone else) from declaring an instance of CacheCollection<T> where T is something that cannot be directly created (if T represented an abstract class, for example). As the error hints, you can require that T be 'creatable' by adding the new() constraint to the generic parameter:

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

You're still going to have a problem, though: There is no way to tell the C# compiler that T will have a constructor that accepts a parameter of type int. Probably the easiest way around that issue is to create an instance of T and then assign its Id property (assuming that EntityBase defines such a property):

T entity = new T { Id = Id };
Daniel Pratt
A: 

I believe the compiler error you are getting tells you the solution. You have to add another type constraint so it knows you can create an instance. All you need to do is add the new()

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

But this will only allow you to create an instance with the default or a parameterless constructor. So you will need either to specify a property on the EntityBase to allow you to set the values you need to or an interface to use by adding another constraint.

Something like:

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

The problem is that a child of EntityBase can't be guaranteed to have the constructor you want.

Also you can use reflection to find and call the constructor:

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

But again this can't be guaranteed at runtime unless you keep up with all your child class implementations.

Sean Copenhaver
A: 

Two things:

// 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;
...
Dave
`Id` may be a read-only property
max
Would then `T entity = new T { Id = Id };` help? (I never tried that in such a case)
Dave
No. If `Id` is a read-only property, Dan Tao's suggestion is probably the simplest and safest solution.
Daniel Pratt
A: 

What I did in similar situations is create an IId interface

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

It is easy to add the interface declaration to your entity classes through the use of partial classes:

public partial MyClass : IId { }

Your class definition now becomes:

public class CacheCollection<T> : List<CacheItem<T>> where T : EntityBase, IId, new()
Jeroen Huinink
A: 
T entity = (T)Activator.CreateInstance(typeof(T), Id);
Mark H