I have a collection of nullable ints.
Why does compiler allows to iteration variable be of type int not int? ?
List<int?> nullableInts = new List<int?>{1,2,3,null};
List<int> normalInts = new List<int>();
//Runtime exception when encounter null value
//Why not compilation exception?
foreach (int i in nullableInts)
{
//do sth
}
Of course I should pay attention to what I iterate through but it would be nice if compiler reprimanded me:) Like here:
foreach (bool i in collection)
{
// do sth
}
//Error 1 Cannot convert type 'int' to 'bool'
Because the C# compiler dereferences the Nullable<T>
for you.
If you write this code:
var list = new List<int?>()
{
1,
null
};
foreach (int? i in list)
{
if (!i.HasValue)
{
continue;
}
Console.WriteLine(i.GetType());
}
foreach (int i in list)
{
Console.WriteLine(i.GetType());
}
The C# compiler produces:
foreach (int? i in list)
{
if (i.HasValue)
{
Console.WriteLine(i.GetType());
}
}
foreach (int? CS$0$0000 in list)
{
Console.WriteLine(CS$0$0000.Value.GetType());
}
Note the explicit dereferencing of Nullable<int>.Value
. This is a testament to how ingrained the Nullable<T>
structure is in the runtime.
OK, originally I said "the compiler adds casts to a foreach
loop." This isn't strictly accurate: it won't always add casts. Here's what's really happening.
First of all, when you have this foreach
loop:
foreach (int x in collection)
{
}
...here is the basic outline (in pseudo-C#) of what the compiler creates:
int x;
[object] e;
try
{
e = collection.GetEnumerator();
while (e.MoveNext())
{
x = [cast if possible]e.Current;
}
}
finally
{
[dispose of e if necessary]
}
What? I hear you saying. What do you mean by [object]
?"
Here's what I mean. The foreach
loop actually requires no interface, which means it's actually a little bit magical. It only requires that the type of object being enumerated exposes a GetEnumerator
method, which in turn must provide an instance of some type that provides a MoveNext
and a Current
property.
So I wrote [object]
because the type of e
does not necessarily have to be an implementation of IEnumerator<int>
, or even IEnumerator
-- which also means it doesn't necessarily have to implement IDisposable
(hence the [dispose if necessary]
part).
The part of the code we care about for the purpose of answering this question is the part where I wrote [cast if possible]
. Clearly, since the compiler doesn't require an actual IEnumerator<T>
or IEnumerator
implementation, the type of e.Current
cannot be assumed to be T
, object
or anything in between. Instead, the compiler determines the type of e.Current
based on the type returned by GetEnumerator
at compile time. Then the following happens:
x
in the above example), a straight assignment is used.e.Current
to the type of x
), a cast is inserted.So in the scenario of enumerating over a List<int?>
, we get to step 2 and the compiler sees that the List<int?>.Enumerator
type's Current
property is of type int?
, which can be explicitly cast to int
.
So the line can be compiled to the equivalent of this:
x = (int)e.Current;
Now, what does the explicit
operator look like for Nullable<int>
?
According to Reflector:
public static explicit operator T(T? value)
{
return value.Value;
}
So the behavior described by Kent is, as far as I can tell, simply a compiler optimization: the (int)e.Current
explicit cast is inlined.
As a general answer to your question, I stick by my assertion that the compiler inserts casts in a foreach
loop where needed.
The compiler automatically inserts casts where needed in a foreach
loop for the simple reason that before generics there was no IEnumerable<T>
interface, only IEnumerable
*. The IEnumerable
interface exposes an IEnumerator
, which in turn provides access to a Current
property of type object
.
So unless the compiler performed the cast for you, in olden times, the only way you could've used foreach
would've been with a local variable of type object
, which obviously would've sucked.
*And actually, foreach
doesn't require any interface at all -- only the GetEnumerator
method and an accompanying type with a MoveNext
and a Current
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With