7

I have a class that manages collections of objects e.g. List<Car> and List<Bike> which are atribute.

I'd like to find a way to get a reference to each of those collections in a lookup so I can implement methods such as Add<Car>(myCar) or Add(myCar) (with reflection) and it will add it to the right collection.

I tried the following,

public class ListManager 
{
    private Dictionary<Type, Func<IEnumerable<object>>> _lookup 
        = new Dictionary<Type, Func<IEnumerable<object>>>();

    public ListManager()
    {
        this._lookup.Add(typeof(Car), () => { return this.Cars.Cast<object>().ToList(); });
        this._lookup.Add(typeof(Bike), () => { return this.Bikes.Cast<object>().ToList(); });
    }

    public List<Car> Cars { get; set; }
    public List<Bike> Bikes { get; set; }
}

but .ToList() creates a new list and not a reference, so _lookup[typeof(Car)]().Add(myCar) is only added to the dictionary list.

Brendan
  • 18,771
  • 17
  • 83
  • 114

3 Answers3

5

This will work:

public class ListManager
{
    private Dictionary<Type, IList> _lookup
        = new Dictionary<Type, IList>();

    public ListManager()
    {
        _lookup.Add(typeof(Car), new List<Car>());
        _lookup.Add(typeof(Bike), new List<Bike>());
    }

    public List<Car> Cars
    {
        get { return (List<Car>)_lookup[typeof(Car)]; }
    }

    public List<Bike> Bikes
    {
        get { return (List<Bike>)_lookup[typeof(Bike)]; }
    }

    public void Add<T>(T obj)
    {
        if(!_lookup.ContainsKey(typeof(T))) throw new ArgumentException("obj");
        var list = _lookup[typeof(T)];

        list.Add(obj);
    }
}

It would be nice if both Car and Bike are derived from the same class or implement the same interface. Then you can add type constraint to the Add method to get compile errors instead of ArgumentException.

Edit

There is a small problem with the simple solution above. It will only work if the type of the objects added to the list is exactly the type stored in the _lookup dictionary. If you try to add an object derived from Car or Bike it will throw.

For example if you define a class

public class Batmobile : Car { }

Then

var listManager = new ListManager();
listManager.Add(new Batmobile());

will throw ArgumentException.

To avoid it you will need a more complicated type lookup method. Instead of simple _lookup[typeof(Car)] it should be:

private IList FindList(Type type)
{
    // find all lists of type, any of its base types or implemented interfaces
    var candidates = _lookup.Where(kvp => kvp.Key.IsAssignableFrom(type)).ToList();

    if (candidates.Count == 1) return candidates[0].Value;

    // return the list of the lowest type in the hierarchy
    foreach (var candidate in candidates)
    {
        if (candidates.Count(kvp => candidate.Key.IsAssignableFrom(kvp.Key)) == 1)
            return candidate.Value;
    }

    return null;
}
Jakub Lortz
  • 14,616
  • 3
  • 25
  • 39
3

Try the following approach:

public class ListManager
{
    private readonly Dictionary<Type, IList> _lookup = new Dictionary<Type, IList>();

    public ListManager()
    {
        _lookup.Add(typeof(Car), new List<Car>());
        _lookup.Add(typeof(Bike), new List<Bike>());
    }

    public List<T> Get<T>()
    {
        return _lookup[typeof(T)] as List<T>;
    }

    public void Add<T>(T item)
    {
        Get<T>().Add(item);
    }

    public List<Car> Cars
    {
        get {  return Get<Car>(); }
    }

    public List<Bike> Bikes
    {
        get { return Get<Bike>(); }
    }
}

Usage:

var listManager = new ListManager();
listManager.Add(new Car());

About derived classes

If you have some class derived from the Car, for example:

public class Ferrari : Car { }

And for some reason you don't want to have a List<Ferrari> in the dictionary, but you want to add Ferrari to the List<Car>. Then you should specify the generic type argument explicitly:

listManager.Add<Car>(new Ferrari());

It's important that the compiler checks that Ferrari is a Car at compile time, so you cannot add Ferrari to List<Bike>.

But in this case it is possible that you'll forget to specify a generic type argument somewhere and therefore you'll get an exception at run time.
To avoid it, just remove the Add<T> method. Thus you'll must explicitly specify a type of the collection each time:

listManager.Get<Car>().Add(new Ferrari());

But all the type checks will be performed at compile time.

Moreover, using the last approach you are able to manipulate lists as you like, since the Get<T> method returns a reference to the fully-functional List<T> (not just pure non-generic IList):

List<Car> cars = listManager.Get<Car>();
cars.Add(new Ferrari());
var coolCars = cars.OfType<Ferrari>();

So you don't need to reinvent the wheel by reimplementing List<T> methods in the ListManager.

Dmitry
  • 13,797
  • 6
  • 32
  • 48
2

You can enumerate all ListManager properties and filter by its type. This is a working example:

public class Car
{
     public int Wheels { get; set; }
}
public class Bike
{
    public int Pedals { get; set; }
}

public class ListManager
{
    //Defina all list properties here:
    public List<Car> Car { get; } = new List<Car>();
    public List<Bike> Bikes { get; } = new List<Bike>();

    //Gets a list instance by its element type
    public object GetList(Type ElemType)
    {
        //Get the first property that is of the generic type List<> and that it's first generic argument equals ElemType,
        //then, obtain the value for that property
        return GetType().GetProperties()
            .Where(x =>
           x.PropertyType.IsGenericType &&
           x.PropertyType.GetGenericTypeDefinition() == typeof(List<>) &&
           x.PropertyType.GetGenericArguments()[0] == ElemType).FirstOrDefault().GetValue(this);
    }

    public void Add(object Value)
    {

        var ElemType = Value.GetType();
        var List = GetList(ElemType);
        //If list had more Add method overloads you should get all of them and filter by some other criteria
        var AddMethod = List.GetType().GetMethod("Add");

        //Invoke the Add method for the List instance with the first parameter as Value
        AddMethod.Invoke(List,new [] { Value });
    }
}

And the test console program:

class Program
{
    static void Main(string[] args)
    {
        var L = new ListManager();

        L.Add(new Car { Wheels = 4 });
        L.Add(new Car { Wheels = 3 });
        L.Add(new Bike { Pedals = 2 });

        //Prints 2:
        Console.WriteLine(L.Car.Count);

        //Prints 1:
        Console.WriteLine(L.Bikes.Count);

        Console.ReadKey();
    }
}

**Note: ** The GetList method could be cached using a Dictionary for improving its performance, since it will return always the same instance for the same type

Rafael
  • 2,642
  • 2
  • 24
  • 30