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 encountering 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开发者_如何学Go> 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.
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.)
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 };
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.
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;
...
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()
T entity = (T)Activator.CreateInstance(typeof(T), Id);
精彩评论