none
Generic collections and Interfaces RRS feed

  • Question

  • Hi, 

         I work a lot with fairly complex generic collections (for example Dictionary<string, List<string[]>>) and encounter the issue that the complier won't accept them as valid instances where the contained classes are interfaces. For example, when I try to pass a
    Dictionary<string, List<string[]>>to a method which takes a Dictionary<string, IEnumerable<string[]>>, I get conversion errors. Obviously I can get around this, but it's a pain and I don't understand why it doesn't work. Anyone know?

    I know I could encapsulate these collections into classes, and I do when it seems appropriate, but does not always seem worth the work.

    Thanks,Ethan


    Ethan Strauss

    Thursday, January 16, 2020 5:45 PM

Answers

  • You're confusing inheritance with generic types, they are not the same thing but easy to get confused. Let's take your example apart and see why. 

    First let's agree on what we mean by type compatibility. Type A is type compatible with type B (meaning we can assign an instance of A to a variable of type B) if one of the following is true:

    - type A has in its inheritance tree type B
    - type B is an interface and type A implements that interface either directly or indirectly

    //Allowed because Base is in the inheritance tree of Derived
    Base a = new Derived();
    
    //Allowed because Derived implements IFoo either directly or indirectly
    IFoo f = new Derived();
    
    class Base
    {
    }
    
    class Derived : Base, IFoo
    {
    }
    
    interface IFoo
    {
    }

    At this point we're dealing with OOP 101. Where people get tripped up is in thinking that generic types impact the inheritance tree, they don't.

    List<string[]> listOfStrings = new List<string[]>();
    IEnumerable<string[]> enumerableOfStrings = listOfStrings;
    
    Dictionary<string, List<string[]>> dictionaryOfLists = new Dictionary<string, List<string[]>>();
    Dictionary<string, IEnumerable<string[]>> dictionaryOfEnumerables = dictionaryOfLists;

    `listOfStrings` is of type `List<string[]>` which is a concrete implementation of List<T>. If you look at that type it inherits from `Object` and implements some interfaces including IEnumerable<string[]> which itself implements IEnumerable. So `listOfStrings` can be assigned to `List<string[]>` variables and `IEnumerable<string[]>` because it is type compatible.

    However the following is not allowed.

    List<IEnumerable<string>> listOfEnumerables = listOfStrings;

    Why not? Because they don't share a base class other than `object`. `listOfStrings`, if you break out the concrete type derives from `Object`, not `List<IEnumerable<string>>` therefore the first type compatibility rule fails. Next we look at interfaces. `listOfStrings` implements, amongst others, `IEnumerable<string[]>`. But `listOfEnumerables` implements `IEnumerable<IEnumerable<string>>`. These are not the same types. This is 2 different concrete implementations of the same generic interface. They are no more compatible than `IEnumerable<double>` and `IEnumerable<string>`. 

    Now you might try to argue that T is `string[]` in the first case and `IEnumerable<string>` in the second and they are compatible. That would be correct but that doesn't have any impact on the inheritance tree. These are 2 completely independent, concrete types and therefore not compatible. The fact that the underlying generic type parameters are compatible doesn't change the higher level type.

    Now at this point you'd need to understand covariance and contravariance but that is a complex topic so I won't. But `IEnumerable<T>` is covariant so this allows you to assign a more derived generic interface type to a less generic interface type. Hence the following works.

    IEnumerable<object> enumerableOfObjects = listOfStrings;

    They don't share an inheritance tree nor interfaces but since `IEnumerable<T>` says T is covariant then the compiler allows the conversion. Each generic type determines whether it is covariant or not. The determination is what you intend to do with the data. Since `IEnumerable<T>` doesn't manipulate any data then it makes sense to be covariant. In other words, enumerating strings works the same as enumerating objects as far as the interface goes. But this is definitely not true in all cases and therefore each interface makes that decision.

    Now back to your original question about `IDictionary<K,V>`. This interface is not covariant and therefore you cannot do what you did for `IEnumerable<T>`. Why not? Your example sums up the reason why it isn't covariant perfectly so let's go back to it.

    Dictionary<string, List<string[]>> dictionaryOfLists = new Dictionary<string, List<string[]>>();
    dictionaryOfLists["First"] = new List<string[]>();
    
    Dictionary<string, IEnumerable<string[]>> dictionaryOfEnumerables = dictionaryOfLists;
    dictionaryOfEnumerables["Second"] = new Collection<string>();

    `dictionaryOfLists` stores `List<string[]>` values in it. That means that at any point you can add or retrieve the list. If `IDictionary` were covariant then `dictionaryOfEnumerables` would compile, it doesn't now. But if it compiled then you would be able to assign any `IEnumerable<T>` as the value in the dictionary. Since it was assigned a dictionary of lists of strings then suddenly it would also contain a collection of strings. In other words you just added a type to the dictionary that the original declaration wouldn't allow. Hence `IDictionary` isn't covariant and you cannot do what you want here.

    The only workaround to interfaces that don't support covariance is to use encapsulation like you mentioned. I guess if you already have existing types defined then you could also use some base interface that your existing types implement. That is probably how I'd solve this problem.


    Michael Taylor http://www.michaeltaylorp3.net

    Thursday, January 16, 2020 10:26 PM
    Moderator

All replies

  • Classes are not Interfaces. A class can implement an Interface.

    What conversion errors are you talking about? Maybe you mean casting errors.

    I know I could encapsulate these collections into classes, and I do when it seems appropriate, but does not always seem worth the work.

    Can you show some code about your issues?

    Thursday, January 16, 2020 9:25 PM
  • You're confusing inheritance with generic types, they are not the same thing but easy to get confused. Let's take your example apart and see why. 

    First let's agree on what we mean by type compatibility. Type A is type compatible with type B (meaning we can assign an instance of A to a variable of type B) if one of the following is true:

    - type A has in its inheritance tree type B
    - type B is an interface and type A implements that interface either directly or indirectly

    //Allowed because Base is in the inheritance tree of Derived
    Base a = new Derived();
    
    //Allowed because Derived implements IFoo either directly or indirectly
    IFoo f = new Derived();
    
    class Base
    {
    }
    
    class Derived : Base, IFoo
    {
    }
    
    interface IFoo
    {
    }

    At this point we're dealing with OOP 101. Where people get tripped up is in thinking that generic types impact the inheritance tree, they don't.

    List<string[]> listOfStrings = new List<string[]>();
    IEnumerable<string[]> enumerableOfStrings = listOfStrings;
    
    Dictionary<string, List<string[]>> dictionaryOfLists = new Dictionary<string, List<string[]>>();
    Dictionary<string, IEnumerable<string[]>> dictionaryOfEnumerables = dictionaryOfLists;

    `listOfStrings` is of type `List<string[]>` which is a concrete implementation of List<T>. If you look at that type it inherits from `Object` and implements some interfaces including IEnumerable<string[]> which itself implements IEnumerable. So `listOfStrings` can be assigned to `List<string[]>` variables and `IEnumerable<string[]>` because it is type compatible.

    However the following is not allowed.

    List<IEnumerable<string>> listOfEnumerables = listOfStrings;

    Why not? Because they don't share a base class other than `object`. `listOfStrings`, if you break out the concrete type derives from `Object`, not `List<IEnumerable<string>>` therefore the first type compatibility rule fails. Next we look at interfaces. `listOfStrings` implements, amongst others, `IEnumerable<string[]>`. But `listOfEnumerables` implements `IEnumerable<IEnumerable<string>>`. These are not the same types. This is 2 different concrete implementations of the same generic interface. They are no more compatible than `IEnumerable<double>` and `IEnumerable<string>`. 

    Now you might try to argue that T is `string[]` in the first case and `IEnumerable<string>` in the second and they are compatible. That would be correct but that doesn't have any impact on the inheritance tree. These are 2 completely independent, concrete types and therefore not compatible. The fact that the underlying generic type parameters are compatible doesn't change the higher level type.

    Now at this point you'd need to understand covariance and contravariance but that is a complex topic so I won't. But `IEnumerable<T>` is covariant so this allows you to assign a more derived generic interface type to a less generic interface type. Hence the following works.

    IEnumerable<object> enumerableOfObjects = listOfStrings;

    They don't share an inheritance tree nor interfaces but since `IEnumerable<T>` says T is covariant then the compiler allows the conversion. Each generic type determines whether it is covariant or not. The determination is what you intend to do with the data. Since `IEnumerable<T>` doesn't manipulate any data then it makes sense to be covariant. In other words, enumerating strings works the same as enumerating objects as far as the interface goes. But this is definitely not true in all cases and therefore each interface makes that decision.

    Now back to your original question about `IDictionary<K,V>`. This interface is not covariant and therefore you cannot do what you did for `IEnumerable<T>`. Why not? Your example sums up the reason why it isn't covariant perfectly so let's go back to it.

    Dictionary<string, List<string[]>> dictionaryOfLists = new Dictionary<string, List<string[]>>();
    dictionaryOfLists["First"] = new List<string[]>();
    
    Dictionary<string, IEnumerable<string[]>> dictionaryOfEnumerables = dictionaryOfLists;
    dictionaryOfEnumerables["Second"] = new Collection<string>();

    `dictionaryOfLists` stores `List<string[]>` values in it. That means that at any point you can add or retrieve the list. If `IDictionary` were covariant then `dictionaryOfEnumerables` would compile, it doesn't now. But if it compiled then you would be able to assign any `IEnumerable<T>` as the value in the dictionary. Since it was assigned a dictionary of lists of strings then suddenly it would also contain a collection of strings. In other words you just added a type to the dictionary that the original declaration wouldn't allow. Hence `IDictionary` isn't covariant and you cannot do what you want here.

    The only workaround to interfaces that don't support covariance is to use encapsulation like you mentioned. I guess if you already have existing types defined then you could also use some base interface that your existing types implement. That is probably how I'd solve this problem.


    Michael Taylor http://www.michaeltaylorp3.net

    Thursday, January 16, 2020 10:26 PM
    Moderator
  • Thats! I think I even understand it.

    So, I guess I was implicitly thinking that if I had 'Dictionary<string,IEnumerable<string>>' any value would be treated as IEnumerable only and not could do nothing other than enumerate. But the fact is that it could be cast back to it's concrete type means you would have all sorts of problems. Is that right?

    Just for my understanding, and very quickly without thinking through all the implications, what do you think of making a concrete class which does nothing but enumerate? Kinda like this:

        public class EnumerableWrapper : IEnumerable
        {
            IEnumerable _Items;
            public EnumerableWrapper(IEnumerable items)
            {
                _Items = items;
            }
    
            public IEnumerator GetEnumerator()
            {
                return _Items.GetEnumerator();
            }
    
            IEnumerator IEnumerable.GetEnumerator()
            {
                return _Items.GetEnumerator();
            }
        }
        public class WrapperTest
        {
            private void blah()
            {
                List<string> listOfStrings = new List<string>();
                IEnumerable<string> enumerableOfStrings = new List<string>();
                Dictionary<string, List<string>> dict = new Dictionary<string, List<string>>();
                dict.Add("list", listOfStrings);
                //       dict.Add("Enumerable", enumerableOfStrings);  //Won't compile
    
                Dictionary<string, EnumerableWrapper> wrapper = new Dictionary<string, EnumerableWrapper>();
                wrapper.Add("list", new EnumerableWrapper(listOfStrings));
                wrapper.Add("Enumerable", new EnumerableWrapper(enumerableOfStrings));
    
                foreach (var item in wrapper)
                {
                    foreach (var subitem in item.Value)
                    {
                    }
                }
            }
    

    This builds without complaining and I suspect you could make it generic.

    Thanks again!

    Ethan


    Ethan Strauss

    Friday, January 17, 2020 5:34 PM
  • There really isn't a benefit of `EnumerableWrapper` because we already have an interface that does the same thing. You cannot make it generic unless you used covariance but `IEnumerable<T>` is already covariant so in this case you wouldn't accomplish anything. You might as well just use `Dictionary<string, IEnumerable>` and you'd accomplish the same thing. However you have now lost the benefits of generics. But as soon as you add generics back into the mix then we're back where we started. If you want to store a dictionary of enumerable objects then just use `IEnumerable<T>` for the original dictionary. Then you can store values of `List<T>` or `string[]` because they are type compatible with `IEnumerable<T>`.

    Dictionary<string, IEnumerable<string>> dictionaryOfStrings = new Dictionary<string, IEnumerable<string>>();
    dictionaryOfStrings["First"] = new List<string>();
    dictionaryOfStrings["Second"] = new string[10];

    The downside is that you cannot modify the list of values. If you need modification then switch from `IEnumerable<T>` to `IList<T>`. 

    var dictionaryOfStrings = new Dictionary<string, IList<string>>();
    dictionaryOfStrings["First"] = new List<string>();
    dictionaryOfStrings["Second"] = new string[10];
    
    dictionaryOfStrings["Second"].Add("Hello");

    Interestingly arrays implement IList<T> but you cannot add/remove items from an array so calling `Add` will result in an exception but the compiler lets it through anyway.

    The general rule of thumb is to use `IEnumerable<T>` for enumeration only. Use `IList<T>` (or a concrete type) if you need to modify the list of items.


    Michael Taylor http://www.michaeltaylorp3.net

    Friday, January 17, 2020 5:49 PM
    Moderator
  • I see your point.

    Thanks again,

    Ethan


    Ethan Strauss

    Friday, January 17, 2020 6:05 PM