3

TL;DR: If List<T> implements ICollection<T>, and Facility implements IAmAHierarchyNode, why can't I implicitly convert List<Facility> to ICollection<IAmAHierarchyNode>?


I'm getting an error of "Cannot implicitly convert type 'Foo' to 'Bar'. An explicit conversion exists (are you missing a cast?)"

Here's the code:

public interface IAmAHierarchyNode<IdType> {
      IdType Id {
         get;set;
      }

      ICollection<IAmAHierarchyNode<IdType>> Children {
         get;
      }

      IAmAHierarchyNode<IdType> Parent {
         get;set;
      }
}

public class Network : IAmAHierarchyNode<Guid> {
   public List<Facility> Facilities
   {
      get;set;
   }

   // This is where it's making me do an explicit conversion. But why?
   public ICollection<IAmAHierarchyNode<Guid>> Children => Facilities;

   public IAmAHierarchyNode<Guid> Parent {
      get => throw new InvalidOperationException ();
      set => throw new InvalidOperationException ();
   }
}

public class Facility : IAmAHierarchyNode<Guid> {
   public Network Network {
      get;set;
   }

   public Guid NetworkId {
      get;set;
   }

   public ICollection<IAmAHierarchyNode<Guid>> Children {
      get => throw new NotImplementedException ();
   }

   public IAmAHierarchyNode<Guid> Parent {
      get => Network;
      set => throw new NotImplementedException ();
   }
}

Here's what I've found so far in my research:

This SO question is the closest I've found. The second answer there says

You can only convert List<T1> to ICollection<T2> if the types T1 and T2 are the same.

So I'm converting from List<Facility> to ICollection<IAmAHierarchyNode>. My Facility class implements IAmAHierarchyNode, so I don't understand why they wouldn't be considered the same.

Sadly, this SO question is a typo on the OP's part. This one is in reference to explicit interface implementations and the answers there would lead me to think that I shouldn't be having a problem.

Jason 'Bug' Fenter
  • 1,616
  • 1
  • 16
  • 27
  • Honestly, too promiscous implicity conversions are bad. Strong typisation is your biggest friend. If you do not have it, look at the PHP and Javascript examples from here: http://www.sandraandwoo.com/2015/12/24/0747-melodys-guide-to-programming-languages/ having actually programm in PHP once, I **wish** I had strong typisation. – Christopher Feb 02 '18 at 23:08
  • The compiler must be thinking that there is risk of losing information, I had the same question some time ago, but read https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/casting-and-type-conversions and decided not to post as strong typing is heavily advised anyway – Vidmantas Blazevicius Feb 02 '18 at 23:20
  • Here we actually have a cast (List to ICollection) and one of the "variance" mechanics for Generics at work (Facility to IAmAHierarchyNode). And I guess the compiler is just not daring enough to try doing both a cast and a variance change. – Christopher Feb 02 '18 at 23:20
  • "You can only convert List to ICollection if the types T1 and T2 are the same." - "So I'm converting from `List` to `ICollection`" - they are not the same. While a `Facility` might be an `IAmAHierarchyNode`, an `IAmAHierarchyNode` is not necessarily a `Facility`. This is why they are not the same and that quote doesn't apply. – Chris Feb 02 '18 at 23:39
  • _"My Facility class implements IAmAHierarchyNode, so I don't understand why they wouldn't be considered the same"_ -- in the post you're referencing, the word "same" is being used to mean **exactly the same**. Not "is a" in the sense of OOP, but literally **the same exact type**. There are exceptions now, using generic type variance with interfaces, but those exceptions do not apply to your example. The _many_ existing questions and answers on Stack Overflow discussion generic type variance already cover your question completely adequately. – Peter Duniho Feb 02 '18 at 23:54
  • Because this is not valid: `Network.Children.Add(new Network());` because a `Network` is not a `Facility` – Wouter Feb 03 '18 at 00:16

2 Answers2

4

You can add the cast but it will not work at runtime.

In .NET different instantiation of generic classes or interfaces are treated as separate types at runtime, so two generic instantiations do not share an inheritance relationship, even if their generic parameters share an inheritance relation:

class A { }
class B: A { }
List<B> b = new List<B>();
ICollection<A> ca = (ICollection<A>)b; // Will cause a runtime error, you can cast at compile time but the cast will fail at runtime.

There is one exception to this rule covariant/contravariant interfaces. IEnumerable<out T> is covariant, so you can assign an enumerable of a derived type to a reference of an enumerable of a base type

List<B> b = new List<B>();
IEnumerable<A> ca = b; // This works without any cast

The problem with allowing the cast you want is that ICollection<T> allows you to add to the collection. In the example above if you did manage to cast an ICollection<B>to ICollection<A>, and you have another derived type class C : A{} you could pass C to the reference of ICollection<A> which actually expects Bs

class A { }
class B: A { }
class C: A { }
List<B> b = new List<B>();
ICollection<A> ca = (ICollection<A>)b; 
ca.Add(new C()); // This would be legal if the above cast would succeed,    
                 // and you would have a List<B> containing a C
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Important quote from the link: For an interface, covariant type parameters can be used as the return types of the interface's methods, and contravariant type parameters can be used as the parameter types of the interface's methods. – riQQ Feb 02 '18 at 23:56
  • Because this is not valid: `Network.Children.Add(new Network());` because a `Network` is not a `Facility` – Wouter Feb 03 '18 at 00:16
  • @Wouter yup, good example – Titian Cernicova-Dragomir Feb 03 '18 at 00:17
1

The reason you cannot do this is that ICollection(T) is Invariant. You could accomplish the behavior you wanted with List<Facility> and IEnumerable<IAmAHierarchyNode>, but I assume that you want to be able to use the methods provided by ICollection(T).

Of course, you can see this difference in the interface definitions:

public interface ICollection<T> : IEnumerable<T>, IEnumerable

public interface IEnumerable<out T> : IEnumerable

The out marks the generic interface as covariant, thus allowing more derived types. Take a look at the following example:

void Main()
{
    var x = new List<A>();
    IEnumerable<I> y = x; // Fine
    ICollection<I> z = x; // Compile Error
}

public class A : I { }

public interface I { }
Jonathon Chase
  • 9,396
  • 21
  • 39