JuniorIncanter
JuniorIncanter

Reputation: 1579

C# Attempting to Use Array Indexing with Generic Typing

So, I've found several answers to half of my issue, but the two pieces I am trying to combine seem to put things together in ways that others haven't run into before (or at least they're not posting about it).

My objective is to be able use my code like this:

    ModelFactory[DataModelX].DataY

What I know I can get is this:

    ModelFactory.Get<DataModelX>.DataY

Admittedly not that much different, but at this point I've felt that I've gotten close and want to see if I can get it.

My current classes are:

public class ModelFactory<T> :  : IReadOnlyList<IDataModel> where T : IDataModel
    private static ModelFactory<IDataModel> _instance;

    private ModelFactory() {}

    public static ModelFactory<IDataModel> ModelFactory => _instance ?? (_instance = new DataModelFactory<IDataModel>());

    private readonly List<IDataModel> _models = new List<IDataModel>();

    private T GetHelper(Type type)
    {
        // check if model exists
        foreach (var model in _models.Where(model => model.GetType() == type))
        {
            // model exists, return it
            return (T) model;
        }
        // model doesn't exist, so we need to create it
        var newModel = CreateModel(type);
        _models.Add(newModel);
        return newModel;
    }

    public T this[
        Type type
    ] {
        get
        {
            return GetHelper(type);
        }
    }

And DataModels like so:

public class DataModel1 : IDataModel
    public string Alpha {get; set;}
    public int Beta {get; set;}
    // etc...

public class DataModel2 : IDataModel
    public foo Cat {get; set;}
    public foobar Dog {get; set;}
    // etc...

And so on.

However, when I try to type ModelFactory.ModelFactory[DataModelX]. I can't get access to ModelFactory. The property is obviously public, and contained within the appropriate class. Also, I can't pass the class as a parameter (of course), but I don't know how to adjust the parameter on the array indexing to accept the class as a parameter.

Constaints:

  1. ModelFactory should be a static class or a singleton.
  2. DataModels are classes that all implement IDataModel.
  3. Data properties are unique to each individual DataModel

Upvotes: 0

Views: 308

Answers (2)

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112279

You can use typeof(T) in order to get the type description of the generic parameter T.

T newModel = (T)CreateModel(typeof(T));

There is no need to have an additional Type parameter, if it always describes the same type as T.

Also you could use a dictionary instead of a list for storing models.

private readonly Dictionary<Type,IDataModel> _models = new Dictionary<Type,IDataModel>();

Then you can get a value with

IDataModel model;
If (!_models.TryGetValue(typeof(T), out model)) {
    model = CreateModel(typeof(T));
    _models.Add(typeof(T), model);
}
return (T)model;

The generic type parameter of your class is misleading as it seems to imply that you are going to create one factory per model type. Just drop it.

Also you cannot implement IReadOnlyList<out T> as it requires the index to be an int. Drop it.

public class ModelFactory { ... }

Now you have two options:

  1. Let the indexer or an ordinary Get-method return a IDataModel and later cast the result

    var m = (MyModelType)factory.Get(typeof(MyModelType));.
    or
    var m = (MyModelType)factory[typeof(MyModelType)];.

  2. Declare a get method with a generic type argument (but no generic type argument for the class!). Indexers cannot have generic type arguments.

public T Get<T>() : where T : IDataModel
{
    ... (same as above)
    return (T)model;
}

call it with

var m = factory.Get<MyModelType>();

Upvotes: 2

Diosjenin
Diosjenin

Reputation: 743

While it would be nice to have, what you are asking for is unfortunately impossible.

There are three possible ways of implementing this factory, none of which do what you want:

1) The implementation you have, which is a generic ModelFactory<T> where T : IDataModel. As has been mentioned, a ModelFactory<T> would force you to limit your factory to take a single type - as in, you would have to define a ModelFactory<DataModel1>, a ModelFactory<DataModel2>, etc. That's a questionable design decision anyway, but more to the point, you can only ever put in and get out the concrete IDataModel type you used to create the factory. For example, you can't create a ModelFactory<DataModel1> and get a DataModel2 from the indexer. So there's not much point to using this implementation.

2) A non-generic ModelFactory with an indexer that returns an IDataModel. This is perfectly valid, but you would have to cast the output to get at the concrete type. So your call syntax using the indexer would be ((DataModel2)ModelFactory[DataModel2]).Cat, or (ModelFactory[DataModel2] as DataModel2).Cat. In my opinion, this is a LOT uglier than a generic Get<T>() function.

3) A non-generic ModelFactory with a generic indexer, such as public T this<T>[Type type] where T : IDataModel. This is closest to what you want, but unfortunately, it is explicitly disallowed under current (C# 6) rules. See, for example, Why it is not possible to define generic indexers in .NET?. It's worth noting, however, that you still wouldn't be able to get the compiler to automatically recognize type as T. You couldn't write public T this<T>[T type] because that would interpret type as an object of type T, nor could you write public T this<T>[typeof(T) type] or something like it, since typeof(T) is an object of type Type. So you would have to call your indexer with ModelFactory<DataModel2>[typeof(DataModel2)].Cat (still ugly!), and implement a runtime check inside to ensure that type is really type T.

So the cleanest option you have is to use a non-generic ModelFactory that retrieves IDataModel implementations from an internal Dictionary<Type, IDataModel>, using public T Get<T>() where T : IDataModel { ... }.

Upvotes: 1

Related Questions