9

Why do I get a compile error in the following code (see line with comment)?

    public void Test()
    {
        HashSet<HashSet<Animal>> setWithSets = new HashSet<HashSet<Animal>>();
        HashSet<Cat> cats = new HashSet<Cat>();
        setWithSets.Add(cats); // Compile error
    }

    private class Animal { }

    private class Cat : Animal { }

VS2012 gives me two errors, the first one the important one:

  • Error 2 Argument 1: cannot convert from 'System.Collections.Generic.HashSet<Expenses.Tests.TestDb.SetTest.Cat>' to 'System.Collections.Generic.HashSet<Expenses.Tests.TestDb.SetTest.Animal>'
  • Error 1 The best overloaded method match for 'System.Collections.Generic.HashSet<System.Collections.Generic.HashSet<Expenses.Tests.TestDb.SetTest.Animal>>.Add(System.Collections.Generic.HashSet)' has some invalid arguments

My question is: Why can I not add "cats" to the "setWithSets"?

Yngvar Kristiansen
  • 1,453
  • 1
  • 18
  • 25
  • 1
    You're looking at Generic Covariance, and you might wanna check out this question: http://stackoverflow.com/questions/245607/how-is-generic-covariance-contra-variance-implemented-in-c-sharp-4-0 or this article (it's using the same objects as you actually) -> http://blogs.msdn.com/b/charlie/archive/2008/10/28/linq-farm-covariance-and-contravariance-in-visual-studio-2010.aspx – Dimitar Dimitrov May 31 '13 at 12:39
  • 1
    Thanks, will check them out. I also found another post that might make this post duplicate: http://stackoverflow.com/questions/6314006/how-do-you-manage-a-c-sharp-generics-class-where-the-type-is-a-container-of-a-ba?rq=1 – Yngvar Kristiansen May 31 '13 at 12:43

4 Answers4

10

To better understand why this is not allowed, consider the following program.

The line setOfSets.First().Add(new Dog()); is acceptable to the compiler, because a collection of animals can surely hold an instance of Dog. The problem is that the first collection of animals in the collection is a collection of Cat instances, and Dog does not extend Cat.

class Animal { }
class Cat : Animal { }
class Dog : Animal { }

class Program {
    static void Main(string[] args) {

        // This is a collection of collections of animals.
        HashSet<HashSet<Animal>> setOfSets = new HashSet<HashSet<Animal>>();

        // Here, we add a collection of cats to that collection.
        HashSet<Cat> cats = new HashSet<Cat>();
        setOfSets.Add(cats);

        // And here, we add a dog to the collection of cats. Sorry, kitty!
        setOfSets.First().Add(new Dog());
    }
}
qxn
  • 17,162
  • 3
  • 49
  • 72
  • 1
    I was about to do the same thing with apples and bananas :) – uriDium May 31 '13 at 12:50
  • Funny thing languages like Java support covariance in arrays, which causes some runtime checks to prevent these type of thing, impacting the performance – Ricardo Rodrigues May 31 '13 at 12:58
  • Marking this as 'answer', as it simple and clearly illustrates the problem with my thinking. However, I really appreciacte the other follow-ups, as they give me deeper insight in the subject. – Yngvar Kristiansen May 31 '13 at 13:00
  • @RicardoRodrigues .NET also has this "crazy" covariance of array types, leading to type checks at all writes, and same performance hit. It's been like that ever since .NET 1 (that is before generics). – Jeppe Stig Nielsen May 31 '13 at 13:06
7

Even if Cat derives from Animal, it is not true that HashSet<Cat> derives from HashSet<Animal>. (The only base class of HashSet<Anything> is the object class.)

To get the behavior you want, the HashSet<T> generic type would need to be covariant in its type parameter T. But it is not, for two reasons:

  1. In C#, only generic interfaces and generic delegate types can be co- or contravariant. HashSet<> is a class.
  2. You can not only read from a HashSet<>, you can also add to it (and do other things). Therefore covariance is logically impossible. Or else one would be able to regard a HashSet<Cat> as a HashSet<Animal> and then add a Dog to it. But a set of cats does not allow dogs.

If you changed HashSet<T> into for example IReadOnlyCollection<T> (see .NET 4.5 documentation: IReadOnlyCollection<out T> Interface), things would work because the latter type (1) is an interface, (2) allows only reads, and (3) has therefore premitted a marking "I'm covariant in T" which the authors of the type decided to apply.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
5

You get a compiler error, because the type constructor of HashSet is invariant.

For an explanation of the term invariant, have a look at Covariance and contravariance

nvoigt
  • 75,013
  • 26
  • 93
  • 142
  • I wouldn't say the _constructor_ was invariant; I would say the class (or type) is invariant (in `T`). – Jeppe Stig Nielsen May 31 '13 at 12:52
  • @JeppeStigNielsen I fixed the wording to match the wiki article. – nvoigt May 31 '13 at 12:53
  • 1
    This would be the correct answer if you changed the phrasing a little, it's not the constructor of HashSet that is invariant, it's HashSet itself in T. Just to add some more info, in order for this to be possible, HashSet would have to be covariant in its type parameter T, like IEnumerable is since .NET 4.0 – Ricardo Rodrigues May 31 '13 at 12:54
2

Because HashSet<Cat> does not derive from HashSet<Animal>, which is required for what you want to do.

What you can do is add a Cat to a HashSet<Animal>, because Cat derives from Animal What you cannot do is add a HashSet<Cat> to a HashSet<HashSet<Animal>>

You probably thought you could use covariance, which allows you to do this:

IEnumerable<Cat> cats = new List<Cat>();
IEnumerable<Animal> animals = cats;

This works because this is the interface declaration for IEnumerable:

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

Notice the 'out T'? That's covariance. It basically allows you to have inheritance-like behavior on generically-typed classes. Note that you can only declare covariance on interfaces. Now let's look at ISet, the interface that HashSet implements:

public interface ISet<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
    ...
}

As you can see, no 'out' keyword. That means you can't do this:

ISet<Cat> cats = new HashSet<Cat>();
ISet<Animal> animals = cats;
Moeri
  • 9,104
  • 5
  • 43
  • 56