C# foreach on IEnumerable<IList<object>> compiles but shouldn't
Asked Answered
G

4

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.

Globin answered 30/11, 2018 at 14:18 Comment(8)
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();Newburg
You get a "suspicious cast" with resharperOskar
@Newburg you are right, but I expect the compiler recognizes the error even with the IList.Globin
@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.Newburg
@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<MyClass>. 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<MyClassChild>() and MyClassChild was a child and implemented IList<MyClass>.Oskar
@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.Sarah
@sepp2k: foreach always had a builtin explicit cast, they can enumerate List<Object> 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.Oskar
@Rango I feel like that should be the answer to the question.Sarah
A
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.

...

Amphictyony answered 30/11, 2018 at 14:26 Comment(11)
I think the third statement is what I was looking for. However, I don't really get the meaning of it.Globin
@Globin The third statement of what? Can you quote it so I can explain?Amphictyony
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.Globin
@Globin 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.Amphictyony
Well, I understand what it means...but I don't understand why such a conversion is allowed.Globin
@Globin let’s say you have a variable of type IList<MyClass> called x. At runtime, you might assign an instance of a subclass of MyClass, which implements IList<MyClass>, to x. In situation, (MyClass)x will succeed.Amphictyony
If we talk about inheritance it is ok. My doubts are about the not sealed part of the statement.Globin
@Globin 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.Amphictyony
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.Globin
@Globin 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.Amphictyony
For me it does not make any sense, I needed further explanation and opened a new question: #53591687Globin
R
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.

Raulrausch answered 30/11, 2018 at 14:26 Comment(4)
"I'm getting a warning for suspicious cast" but that' from resharper isn't it?Oskar
@Rango It could, tbh it looks like a native warning but I've been working with resharper so long that I can't notice the difference.Raulrausch
Not a Native warning. I have stock VS 2017 and it shows no errors or warnings.Armalla
@Armalla it must be Resharper thenRaulrausch
L
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).

Lipp answered 30/11, 2018 at 14:25 Comment(0)
A
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.

...

Amphictyony answered 30/11, 2018 at 14:26 Comment(11)
I think the third statement is what I was looking for. However, I don't really get the meaning of it.Globin
@Globin The third statement of what? Can you quote it so I can explain?Amphictyony
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.Globin
@Globin 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.Amphictyony
Well, I understand what it means...but I don't understand why such a conversion is allowed.Globin
@Globin let’s say you have a variable of type IList<MyClass> called x. At runtime, you might assign an instance of a subclass of MyClass, which implements IList<MyClass>, to x. In situation, (MyClass)x will succeed.Amphictyony
If we talk about inheritance it is ok. My doubts are about the not sealed part of the statement.Globin
@Globin 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.Amphictyony
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.Globin
@Globin 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.Amphictyony
For me it does not make any sense, I needed further explanation and opened a new question: #53591687Globin
A
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
}
Aerial answered 30/11, 2018 at 14:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.