Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iteration variable of different type than collection?

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'
like image 851
nan Avatar asked Sep 14 '10 13:09

nan


2 Answers

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.

like image 173
Kent Boogaart Avatar answered Oct 29 '22 20:10

Kent Boogaart


Update

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:

  1. If the type is the type of the local variable (x in the above example), a straight assignment is used.
  2. If the type is convertible to the type of the local variable (by which I mean, a legal cast exists from the type of e.Current to the type of x), a cast is inserted.
  3. Otherwise, the compiler will raise an error.

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.


Original Answer

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.

like image 34
Dan Tao Avatar answered Oct 29 '22 21:10

Dan Tao