none
Add list of generic type that implements and interface to a dictionary that has a value of the same interface RRS feed

  • Question

  • I have this code:

    private Dictionary<TypeList<ICacheable>> cachedEntities;
     
    public ICacheable GetFromCache<T>(Expression<Func<Tbool>> predicate) where T : ICacheable
    {
        var type = typeof(T);
     
        if (!cachedEntities.ContainsKey(type))
            cachedEntities.Add(type, new List<T>());
     
        var entitiesList = cachedEntities[type].AsQueryable();
        var result = entitiesList.Where(predicate);
     
        return result;
    }

    I'm getting an error when trying to add the new List<T> to the dictionary claiming that it can't convert from T to ICacheable, but I force T to implement, so I'm not really understanding why I'm getting it.

    I'm also getting an error in the entitiesList.Where(predicate) claiming it can't convert from Expression<Func<T,bool>> to Expression<Func<ICacheable, int, bool>>.

    Thursday, August 30, 2018 1:58 PM

All replies

  • I would guess this is a covariance/contravariance problem. But...since the Dictionary is declared as only holding lists of ICacheable, does that part of the method actually need to use the generic type at all?

    E.g. Could you not do:

     private Dictionary<Type, List<ICacheable>> cachedEntities;
    
    public IQueryable<ICacheable> GetFromCache<T>(Expression<Func<ICacheable, bool>> predicate)
    {
        var type = typeof(T);
    
        if (!cachedEntities.ContainsKey(type))
            cachedEntities.Add(type, new List<ICacheable>());
    
        var entitiesList = cachedEntities[type].AsQueryable();
        var result = entitiesList.Where(predicate);
    
        return result;
    }
    

    (Had to change the return type of the method to IQueryable as well since that's what the Where() method returns).

    Thursday, August 30, 2018 2:39 PM
  • Try this example.

    List<ICacheable> newList = new List<T>();

    Notice that you get an error here. The error is clarifying a fundamental design principal of generics that most people seem to misunderstand. When you hear about generics almost everyone immediately relates them to inheritance and that is not correct. Look at a more concrete example.

    List<object> newList = new List<string>();

    This won't compile either. If you think about it for a minute it probably makes sense. While string derives from object, generics don't imply inheritance. A List<String> does not derive from List<object> just because string derives from object. List<String> derives from List<T> which derives from (ultimately) object. Nowhere in its inheritance tree does List<object> show up. Hence you cannot take a List<T> and assign it to List<S> irrelevant of the relationship between S and T, they are completely different types.

    This might seem like an odd rule but it has everything to do with correctness. Imagine for a moment that this were allowed. 

    List<string> names = ...;
    List<object> values = names;
    
    //Now I can do this
    values.Add(10);
    values.Add(40);
    
    

    Values and names is the same object. I just inserted a couple of integers into a list that only supports strings. Going back to your generic method. While T may implement ICacheable, List<ICacheable> and List<T> are different types. 

    //Assume this call
    var item = GetFromCache<MyCache>(...);
    
    //In your generic method T : MyCache -> ICacheable
    List<ICacheable> newList = new List<MyCache>();
    newList.Add(SomeOtherCacheType);

    Hence you cannot assign generics of one type to generics of another type even if there is a inheritance hierarchy between the 2 types - unless the implementations support covariance as RJP mentioned.

    So how do you work around this? Honestly I think the best option is to simply use object. If the cache is indeed private then you can control access to it. I'd also probably reduce the expression down to a function because an expression isn't gaining you much here. Since it looks like you are looking for a particular item you'll also need to select the top one.

    private Dictionary<Type, List<object>> cachedEntities; public ICacheable GetFromCache<T> ( Func<T, bool> predicate ) where T : ICacheable { var type = typeof(T); var newList = new List<object>(); if (!cachedEntities.ContainsKey(type)) cachedEntities.Add(type, newList); var entitiesList = cachedEntities[type].OfType<T>(); var result = entitiesList.Where(predicate); return result.FirstOrDefault(); }

    If you still want to use the expression then you'll have to compile it first so it can be used.

    As an aside I'd also question why you'd create a list of items just because you queried for them. If the cache doesn't contain an item then just return null. Probably not any benefit in creating the list and then searching it when you know the item isn't going to be there.


    Michael Taylor http://www.michaeltaylorp3.net

    Thursday, August 30, 2018 9:41 PM
    Moderator
  • ...
    Imagine for a moment that this were allowed. 

    List<string> names = ...;
    List<object> values = names;
    
    //Now I can do this
    values.Add(10);
    values.Add(40);

    Values and names is the same object.
    I just inserted a couple of integers into a list that only supports strings. 


    Michael Taylor

    Bad conclusions...

    Notice what you wrote:

            Values and names is the same object.

    Clearly, names and values ARE DIFFERENT C# OBJECTS; one is of type List<string> and the other is of type List<object>. 

    You also wrote: 

            I just inserted a couple of integers into a list that only supports strings

    Clearly again, you are wrong; you create a new C# object ( List<object> ), therefore, it accepts any C# object and not just string!

    I have no idea why you think that List<object> would accept only string!


    • Edited by ritehere42 Monday, September 3, 2018 2:43 AM typos
    Monday, September 3, 2018 2:40 AM
  • Greetings ritehere42. How are you these days?

    CoolDadTX was showing an example of code that does not work. His aim was to illustrate the point of why there are some things you should not be able to do.

    He was saying that, if that code were allowed, it would make the two lists the same object when (as you point out) they are different. That's why such code is not allowed.

    Monday, September 3, 2018 3:47 AM
  • Check an alternative approach too:

    static class MyCache<T> // (if required, add 'where T : ICacheable')
    {
       private static readonly List<T> list = new List<T>();
    
       public static IEnumerable<T> Get( Func<T, bool> predicate )
       {
          return list.Where( predicate );
       }
    
       public static void Add( T entity )
       {
          list.Add( entity );
       }
    }


    • Edited by Viorel_MVP Monday, September 3, 2018 5:53 AM
    Monday, September 3, 2018 5:52 AM
  • Greetings ritehere42. How are you these days?

    CoolDadTX was showing an example of code that does not work. His aim was to illustrate the point of why there are some things you should not be able to do.

    He was saying that, if that code were allowed, it would make the two lists the same object when (as you point out) they are different. That's why such code is not allowed.

    @Ante Meridian,

    I'm not referring to the assignment, but to the concepts contained in the explanation...

    >> ...it would make the two lists the same object...

    No. If such syntax were allowed, clearly, they'd result in 2 C# objects, because the 2 objects have different type.

    Why do you think they'd have to be the same object?

    Check this example, where the assignment results in 2 (thus different) objects:   

        public class A{...}
        public class B{...} // instances of B are implicitly convertible to instances of A.

        B b = new B();
        A a=b;

    Notice that a and b are not the same C# object, despite the assignment...

    Conclusion:

    If List<object> listOfObjects = listOfStrings; were allowed, they'd result in 2 (thus different) objects,i.e., listOfObjects and listOfStrings would refer to different C# objects.

    Also worthy to notice, that the code below is perfectly valid:

        List<string> listStrings=new List<string>{"a","b"};

        List<object> listObjects=new List<object>(listStrings);
        // instead of: 
        // List<object> listObjects=listStrings;

    Monday, September 3, 2018 6:10 AM