Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing a lookup table that takes T and returns attributes on method of type List<T>

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.

like image 574
Brendan Avatar asked Oct 18 '15 11:10

Brendan


2 Answers

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;
}
like image 190
Jakub Lortz Avatar answered Oct 20 '22 01:10

Jakub Lortz


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.

like image 38
Dmitry Avatar answered Oct 20 '22 01:10

Dmitry