Создание универсального типа? - PullRequest
3 голосов
/ 23 августа 2010

В настоящее время я работаю над общим классом коллекции и хотел бы создать метод, который возвращает объект из коллекции.Если конкретный объект не существует в коллекции, он должен быть создан, добавлен в коллекцию и затем возвращен.

Я столкнулся с несколькими проблемами.Родовая коллекция типа, представляющего абстрактный класс, и у меня возникают проблемы с его созданием.

Вот мое определение класса:

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 голосов
/ 23 августа 2010

ОБНОВЛЕНИЕ 2 : Вы говорите в комментарии, что не определили неуниверсальный тип CacheCollection;но затем вы продолжаете говорить, что у вас есть Dictionary<Type, CacheCollection>.Оба эти утверждения не могут быть правдой, поэтому я предполагаю, что под CacheCollection вы подразумеваете CacheCollection<EntityBase>.

. Теперь вот проблема: X<Derived> не может быть приведен к X<Base>, если X<T>тип не является ковариантным .То есть, в вашем случае, то, что 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<Type, CacheCollection<EntityBase>> определите его как Dictionary<Type, ICacheCollection>, а остальной код должен собраться вместе.


ОБНОВЛЕНИЕ : Похоже, что выот нас воздерживались!Итак, у вас есть неуниверсальный базовый класс CacheCollection, из которого происходит CacheCollection<T>, я прав?

Если мое понимание вашего последнего комментария к этому ответу верно, вот мой совет для вас.Напишите класс, чтобы обеспечить косвенный доступ к вашему Dictionary<Type, CacheCollection>.Таким образом, вы можете иметь множество CacheCollection<T> экземпляров без ущерба для безопасности типов.

Примерно так ( примечание: код, измененный на основе 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, но не имеет конструктора без параметров, вашЛучше всего будет указать фабричный метод, который сгенерирует T для соответствующих параметров в конструкторе CacheCollection<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);
    }
}

Я также рекомендовал бы, если ваши товары будут иметь уникальные идентификаторы, использовать Dictionary<int, CacheItem<T>> в качестве резервного хранилища вместо List<CacheItem<T>> так как это сделает поиск элементов O (1) вместо O (N).

(я бы также рекомендовал реализовать этот класс с использованием закрытого члена для хранения самой коллекции, а не наследования от коллекции напрямую,как использование наследования выставляет Fнеобъективность, которую вы, вероятно, хотите скрыть, например Add, Insert и т. д.)

0 голосов
/ 23 августа 2010
T entity = (T)Activator.CreateInstance(typeof(T), Id);
0 голосов
/ 23 августа 2010

Что я делал в подобных ситуациях, так это создавал интерфейс 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 голосов
/ 23 августа 2010

Две вещи:

// 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 голосов
/ 23 августа 2010

Я полагаю, что ошибка компилятора, которую вы получаете, подсказывает вам решение. Вы должны добавить другое ограничение типа, чтобы он знал, что вы можете создать экземпляр. Все, что вам нужно сделать, это добавить 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 голосов
/ 23 августа 2010

В настоящее время ничто не мешает вам (или кому-то еще) объявить экземпляр CacheCollection<T>, где T - это нечто, что не может быть создано напрямую (например, если T представляет абстрактный класс). Как подсказывает ошибка, вы можете потребовать, чтобы T был «создаваемым», добавив ограничение new() к универсальному параметру:

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

Тем не менее у вас все еще будет проблема: Невозможно сообщить компилятору C #, что T будет иметь конструктор, который принимает параметр типа int. Вероятно, самый простой способ обойти эту проблему - создать экземпляр T и затем присвоить его свойство Id (при условии, что EntityBase определяет такое свойство):

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