views:

318

answers:

3

I've just run into what I think is an oddity in type-casting. I have code similar to the following:

interface IMyClass { }

class MyClass: IMyClass { }

class Main
{
  void DoSomething(ICollection<IMyClass> theParameter) { }

  HashSet<MyClass> FillMyClassSet()
  {
    //Do stuff
  }

  void Main()
  {
    HashSet<MyClass> classSet = FillMyClassSet();
    DoSomething(classSet);
  }
}

When it gets to DoSomething(classSet), the compiler complains that it can't cast HashSet<MyClass> to ICollection<IMyClass>. Why is that? HashSet implements ICollection, MyClass implements IMyClass, so why isn't the cast valid?

Incidentally this isn't hard to work around, thought it's slightly awkward.

void Main()
{
  HashSet<MyClass> classSet = FillMyClassSet();
  HashSet<IMyClass> interfaceSet = new HashSet<IMyClass>();
  foreach(IMyClass item in classSet)
  {
    interfaceSet.Add(item);
  }
  DoSomething(interfaceSet);
}

To me, the fact that this works makes the inability to cast even more mysterious.

+7  A: 

It won't work because all instances of MyClass being IMyClass doesn't automatically imply that all instances of HashSet<MyClass> are also HashSet<IMyClass>. If it worked, you could:

ICollection<IMyClass> h = new HashSet<MyClass>();
h.Add(new OtherClassThatImplementsIMyClass()); // BOOM!

Technically, it doesn't work because C# (<= 3.0) generics are invariant. C# 4.0 introduces safe-covariance, which doesn't help in this case either. It does, however, help when interfaces use the type parameters only in input or only in output positions. For instance, you'll be able to pass a HashSet<MyClass> as an IEnumerable<IMyClass> to some method.

By the way, they are easier workarounds than manually filling another HashSet like:

var newSet = new HashSet<IMyClass>(originalSet);

or you can use the Cast method if you want to cast a set to an IEnumerable<IMyClass>:

IEnumerable<IMyClass> sequence = set.Cast<IMyClass>(set);
Mehrdad Afshari
+1 Bingo.....15
JMD
If I'd given myself more time I should have realized that eventually. The 4.0 information is interesting. So if I can change it to DoSomething(IEnumerable<IMyClass> theParameter), in 4.0 the cast would work? That is, assuming I should really be using IEnumerable.
JamesH
Yes; in 4.0 that would work and it'll be safe. In 3.0, you'd have to use the `Cast` method (even though it's safe, the compiler and runtime have no idea about it).
Mehrdad Afshari
+2  A: 

Such a cast would actually not be safe.

Consider this code:

class MyOtherClass: IMyClass { }

void DoSomething(ICollection<IMyClass> theParameter) { theParameter.Add(new MyOtherClass(); } 

ICollection<MyClass> myClassSet = ...;
DoSomething(classSet);   //Oops - myClassSet now has a MyOtherClass

What you're asking for is called covariance; it's available for IEnumerable<T> (which is read-only) in C# 4.

SLaks
A: 

Its not an oddity, its a limitation of the generic type variance in 2.0. This blog post describes the situation relatively well. They've made some improvements in this in the 4.0 CLR, IIRC.

Will