7

I have the following code:

IEnumerable<IList<MyClass>> myData = //...getMyData

foreach (MyClass o in myData)
{
    // do something
}

It compiles, runs and obviously I get an System.InvalidCastException.
Why does the compiler not complain? MyClass is a simple bean, no extensions.

Edit 1:
As suggested by David switching the type from IList to List the compiler complains

Edit 2:
I've understood that the behaviour is as specified in the C# Language definition. However, I don't understand why such a cast/conversion is allowed, since at runtime I always get an InvalidCastException. I opened this in order to go deeper.

Emaborsa
  • 2,360
  • 4
  • 28
  • 50
  • Note: While replicating this issue, changing from `IList` to `List` produces the compile-time error. Also, while it's still `IList`, doing this produces the compile-time error: `MyClass x = myData.First();` – David Nov 30 '18 at 14:24
  • You get a "suspicious cast" with resharper – Tim Schmelter Nov 30 '18 at 14:28
  • @David you are right, but I expect the compiler recognizes the error even with the IList. – Emaborsa Nov 30 '18 at 14:32
  • 1
    @Emaborsa: Agreed, and the responses below have been enlightening for me on the subject. Just adding to the info, since I found this one pretty interesting. – David Nov 30 '18 at 14:33
  • 2
    @Emaborsa: the compiler does not do this code analysis(while resharper does it). Because actually it's possible that there is a type which inherits from `MyClass` and also implements `IList`. The compiler does not check what you assign, it checks the declared type on the left side. The code would be perfectly valid if you'd assign `new List()` and `MyClassChild` was a child and implemented `IList`. – Tim Schmelter Nov 30 '18 at 14:36
  • @Rango Outside of a `foreach` that would still require a downcast though. That is, it's not legal to write `MyClass foo = myData.GetEnumerator().Current;` - it would need an explicit cast to `MyClass`. Downcasts aren't usually implicit. So the question remains why `foreach` inserts such a cast automatically. I assume the answer is so users were able to write `foreach (MyType x in myUntypedList)` in the days before generics. – sepp2k Nov 30 '18 at 14:51
  • 2
    @sepp2k: `foreach` always had a builtin explicit cast, they can enumerate `List` and cast to whatever you specify in the loop variable-type. That will fail at runtime. Of course the reason is that the `foreach` is much older than generics. – Tim Schmelter Nov 30 '18 at 15:01
  • @Rango I feel like that should be the answer to the question. – sepp2k Nov 30 '18 at 15:04

4 Answers4

6

Well because IList<MyClass> is an interface so theoretically you could have a class that implemented that interface AND derives from MyClass.

If you change it to IEnumerable<List<MyClass>> it will not compile.

In any case, at least I'm getting a warning for suspicious cast, as there is no class in the solution which inherits from both IList<MyClass> and MyClass.

Pinx0
  • 1,248
  • 17
  • 28
5

When a foreach is compiled it follows a pattern not specific types (much as LINQ and await do).

foreach isn't looking for an IEnumerable or IEnumerable<T> but for a type which has a GetEnumerator() method (which IList<T> does). And the objects in the outer list could be of a type derived from MyClass and implementing IList<T>).

Ie. the compiler does a lightweight "matches the pattern" check not a complete check.

See §8.8.3 of the C#5 Language Specification which covers this in detail (and you'll see I've rather simplified things above: even IEnumerator isn't checked for, just that there is a MoveNext() method and a Current property).

Richard
  • 106,783
  • 21
  • 203
  • 265
4

IList<MyClass> is convertible to MyClass.

But if you actually run it with a non-empty enumerable,

IEnumerable<IList<MyClass>> myData = new IList<MyClass>[1] { new List<MyClass>() {new MyClass()}};

You get this error:

Unable to cast object of type 'System.Collections.Generic.List`1[MyClass]' to type 'MyClass'.

This is in compliance with the spec:

Section 8.8.4 The foreach statement

... If there is not an explicit conversion (§6.2) from T (the element type) to V (the local-variable-type in the foreach statement), an error is produced and no further steps are taken.

...

There is an explicit conversion from IList<MyClass> to MyClass (though it will fail at runtime), so no error is produced.

Section 6.2.4 Explicit reference conversions

The explicit reference conversions are:

  • From object and dynamic to any other reference-type.
  • From any class-type S to any class-type T, provided S is a base class of T.
  • From any class-type S to any interface-type T, provided S is not sealed and provided S does not implement T.
  • From any interface-type S to any class-type T, provided T is not sealed or provided T implements S.

...

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I think the third statement is what I was looking for. However, I don't really get the meaning of it. – Emaborsa Nov 30 '18 at 15:01
  • @Emaborsa The third statement of what? Can you quote it so I can explain? – Sweeper Nov 30 '18 at 15:05
  • I thought you ment `From any class-type S to any interface-type T, provided S is not sealed and provided S does not implement T` as answer to my question. – Emaborsa Nov 30 '18 at 15:12
  • @Emaborsa Ah! I copied the wrong thing. Added the corrected one: `From any interface-type S to any class-type T, provided T is not sealed or provided T implements S.` `MyClass` is not sealed. So you can convert from any interface to `MyClass`. – Sweeper Nov 30 '18 at 15:16
  • Well, I understand what it means...but I don't understand why such a conversion is allowed. – Emaborsa Nov 30 '18 at 16:14
  • @Emaborsa let’s say you have a variable of type `IList` called `x`. At runtime, you might assign an instance of a subclass of `MyClass`, which implements `IList`, to `x`. In situation, `(MyClass)x` will succeed. – Sweeper Nov 30 '18 at 16:20
  • If we talk about inheritance it is ok. My doubts are about the `not sealed` part of the statement. – Emaborsa Dec 03 '18 at 06:58
  • @Emaborsa this only works if the class is not sealed. If the class is sealed, then it cannot have subclasses, which eliminates the possibility of the variable actually storing a subclass of the class that implements the interface. – Sweeper Dec 03 '18 at 07:19
  • Yes I understand, but give me an example where such a casting is needed where T is NOT sealed and T does NOT implement S. – Emaborsa Dec 03 '18 at 07:56
  • @Emaborsa I can’t give you an actual example in production code because I have never encountered such situations myself. Here’s a hypothetical one: you have a variable of type `IFlyable` interface and a `Bird` abstract class. Not all birds can fly so `Bird` does not implement `IFlyable`. You want to run some code only if the variable stores a `Bird` but you don’t care if it is `Sparrow` or `Owl` or some other bird that can fly. So you check whether that variable `is Bird` and then cast from `IFlyable` to `Bird`. – Sweeper Dec 03 '18 at 08:21
  • For me it does not make any sense, I needed further explanation and opened a new question: https://stackoverflow.com/questions/53591187/explanation-for-c-sharp-language-specification-6-2-4-explicit-reference-convers – Emaborsa Dec 03 '18 at 09:54
2

Under the assumption that MyClass does not implement IList<MyClass>, there could be a derived type of MyClass that does implement IList<MyClass> and then your loop would be valid.

That is,

class MyClass
{
}

class Derived : MyClass, IList<MyClass>
{
    // ...
}

// ...

// Here IList<MyClass> is Derived, which is valid because Derived implements IList<MyClass>
IEnumerable<IList<MyClass>> myData = new []{new Derived()};

// Here MyClass is Derived, which is valid because Derived inherits from MyClass
foreach (MyClass o in myData)
{
    // do something
}
Theraot
  • 31,890
  • 5
  • 57
  • 86