20

I have the following method in an external class

public static void DoStuffWithAnimals(IDictionary<string, Animal> animals)

In my calling code, I already have a Dictionary<string, Lion> object, but I can't pass this in as this method's argument. So a IDictionary<,> isn't contravariant? I can't see any reason why this shouldn't work.

The only solution I can think of is:

var animals = new Dictionary<string, Animal>();

foreach(var kvp in lions) {
    animals.Add(kvp.Key, kvp.Value);
}

Is there no way to pass this dictionary into this method, without having to create a new dictionary of the same objects?


EDIT:

As it's my method, I know that the only member I'm using from the dictionary is the getter of TValue this[TKey key], which is a member of IDictionary<TKey, TValue>, so in this scenario, I'm unable to use a 'wider' type for the parameter.

Connell
  • 13,925
  • 11
  • 59
  • 92

6 Answers6

9

Firstly, covariance and contravariance in C# only apply to interfaces and delegates.

So your question is really about IDictionary<TKey,TValue>.

With that out of the way, it's simplest to just remember that an interface can only be co/contra-variant if all values of a type parameter are either only passed in, or only passed out.

For example (contravariance):

interface IReceiver<in T> // note 'in' modifier
{
    void Add(T item);
    void Remove(T item);
}

And (covariance):

interface IGiver<out T> // note 'out' modifier
{
    T Get(int index);
    T RemoveAt(int index);
}

In the case of IDictionary<TKey,TValue>, both type parameters are used in both an in and out capacity, meaning that the interface cannot be covariant or contravariant. It is invariant.

However, the class Dictionary<TKey,TValue> does implement IEnumerable<T> which is covariant.

A great reference for this is:

https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • IReadOnlyDictionary could be contravariant then? – Slugart May 19 '16 at 13:15
  • 3
    @Slugart, interestingly not. `TKey` cannot be `in` because it's returned via the `Keys` property. `TValue` cannot be out because it's a parameter of `TryGetValue` (though actually it's an `out` param, so that seems less convincing). Also, `IReadOnlyDictionary` implements `IReadOnlyCollection>` and any `in`/`out` modifier would need to be applied there too, but `KeyValuePair` is not an interface. So all in all, there are three reasons why it cannot be variant. – Drew Noakes May 21 '16 at 14:07
  • `in` is linked to contravariance, and `out` is linked to covariance. See here for a [reference](https://stackoverflow.com/questions/3445631/still-confused-about-covariance-and-contravariance-in-out). – participant Jan 14 '19 at 08:50
7

Solution wise you could do something like this, just pass in an accessor instead:

    public static void DoStuffWithAnimals(Func<string, Animal> getAnimal)
    {
    }

    var dicLions = new Dictionary<string, Lion>();
    DoStuffWithAnimals(s => dicLions[s]);

Obviously that is likely to be a bit simple for your needs, but if you only need a couple of the dictionary methods it's pretty easy to get that in place.

This is another way that gives you a bit of code re-use between your animals:

    public class Accessor<T> : IAnimalAccessor where T : Animal
    {
        private readonly Dictionary<string, T> _dict;

        public Accessor(Dictionary<string, T> dict)
        {
            _dict = dict;
        }

        public Animal GetItem(String key)
        {
            return _dict[key];
        }
    }

    public interface IAnimalAccessor
    {
        Animal GetItem(string key);
    }

    public static void DoStuffWithAnimals(IAnimalAccessor getAnimal)
    {
    }

    var dicLions = new Dictionary<string, Lion>();
    var accessor = new Accessor<Lion>(dicLions);
    DoStuffWithAnimals(accessor);
Andrew Barrett
  • 19,721
  • 4
  • 47
  • 52
  • That first one is fine for my case actually, that's exactly what I need :) @Laurence's answer would also work in my case too. – Connell Oct 11 '12 at 14:38
4

Suppose that Derived is a subtype of Base. Then, Dictionary<Base> can't be a subtype of Dictionary<Derived> because you can't put any object of type Base into a Dictionary<Derived>, but Dictionary<Derived> can't be a subtype of Dictionary<Base> because if you get an object out of a Dictionary<Base>, it might not be a Derived. Therefore, Dictionary is neither co- nor contravariant in its type parameter.

Generally, collections that you can write to are invariant for this reason. If you have an immutable collection, then it could be covariant. (If you had some sort of write-only collection, itmight be contravariant.)

EDIT: And if you had a "collection" that you could neither get data from nor put data into, then it might be both co- and contravariant. (It would also probably be useless.)

Thom Smith
  • 13,916
  • 6
  • 45
  • 91
  • 2
    For example, `DoStuffWithAnimals` is permitted to add a `Puppy` to `animals`, but that should be illegal if `animals` is a collection of `Lion` (unless the lions are hungry). – Brian Oct 11 '12 at 14:13
  • Ah yes, ofcourse! So in my case, it's read-only and the method won't be adding any puppies in with my lions. The only member it will use is the getter of `TValue this[TKey key]` (which is a member of `IDictionary`. Is there still no workaround? – Connell Oct 11 '12 at 14:23
3

You could modify public static void DoStuffWithAnimals(IDictionary<string, Animal> animals) to add a generic constraint. Here is something that works for me in LINQPad:

void Main()
{
    var lions = new Dictionary<string, Lion>();
    lions.Add("one", new Lion{Name="Ben"});
    AnimalManipulator.DoStuffWithAnimals(lions);
}

public class AnimalManipulator
{
    public static void DoStuffWithAnimals<T>(IDictionary<string, T> animals)
    where T : Animal
    {
        foreach (var kvp in animals)
        {
            kvp.Value.MakeNoise();
        }
    }
}

public class Animal
{
    public string Name {get;set;}
    public virtual string MakeNoise()
    {
        return "?";
    }
}

public class Lion : Animal
{
    public override string MakeNoise()
    {
        return "Roar";
    }
}
Laurence
  • 1,673
  • 11
  • 16
2
  1. I believe what you want is called covariance, not contravariance.
  2. Classes in .Net can't be co- or contravariant, only interfaces and delegates can.
  3. IDictionary<TKey, TValue> can't be covariant, because that would allow you to do something like:

    IDictionary<string, Sheep> sheep = new Dictionary<string, Sheep>();
    IDictionary<string, Animal> animals = sheep;
    animals.Add("another innocent sheep", new Wolf());
    
  4. There is IReadOnlyDictionary<TKey, TValue> in .Net 4.5. At first sight it could be covariant (the other new interface, IReadOnlyList<T> is covariant). Unfortunately, it isn't, because it also implements IEnumerable<KeyValuePair<TKey, TValue>> and KeyValuePair<TKey, TValue> is not covariant.

  5. To work around this, you could make your method generic with a constraint of the type parameter:

    void DoStuffWithAnimals<T>(IDictionary<string, T> animals) where T : Animal
    
svick
  • 236,525
  • 50
  • 385
  • 514
  • **1.** Contravariance is converting from narrower to wider. Is that not what I'm trying to do? **2.** Whoops. I knew that, I've changed the question to refer to `IDictionary` when I asked that part. **3.** Aha, yes. This is what I've learned from the other answers. **4.** Bugger. – Connell Oct 11 '12 at 14:32
  • Re 1., it's not that simple and I think the explanation at Wikipedia is confusing. [Quoting Eric Lippert:](http://blogs.msdn.com/b/ericlippert/archive/2007/10/16/covariance-and-contravariance-in-c-part-one.aspx) “Consider an “operation” which manipulates types. If the results of the operation applied to any `T` and `U` always results in two types `T’` and `U’` with the same relationship as `T` and `U`, then the operation is said to be “covariant”. If the operation reverses bigness and smallness on its results […] then the operation is said to be “contravariant”.” – svick Oct 11 '12 at 14:45
0

If the dictionary's interface was read-only (allowing you only to read the key-value pairs, and couldn't even allow the ability to pull a value by its key), it could be marked with the out generic parameter modifier. If the dictionary's interface was write-only (allowing you to insert values, but not to retrieve them or iterate over them), it could be marked with the in generic parameter modifier.

So, you can create the interface yourself and extend the Dictionary class in order to get the functionality you need.

M.A. Hanin
  • 8,044
  • 33
  • 51