开发者

Iteration variable of different type than collection?

开发者 https://www.devze.com 2023-01-16 15:32 出处:网络
I have a collection of nullable ints. Why does compiler allows to iteration variable be of type int not int? ?

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&开发者_StackOverflow中文版gt; 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.


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.


The behavior you are observing is according to section 8.8.4 The foreach statement of the C# language specification. This section defines the semantics of the foreach statement as follows:

[...] The above steps, if successful, unambiguously produce a collection type C, enumerator type E and element type T. A foreach statement of the form

foreach (V v in x) embedded-statement

is then expanded to:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
        while (e.MoveNext()) {
            v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        // Dispose e
    }
}

According to the rules defined in the specification, in your sample the collection type would be List<int?>, the enumerator type would be List<int?>.Enumerator and the element type would be int?.

If you fill this information into the above code snippet you will see that int? is explicitly cast to int by calling Nullable<T> Explicit Conversion (Nullable<T> to T). The implemenation of this explicit cast operator is, as described by Kent, to simply return the Nullable<T>.Value property.

0

精彩评论

暂无评论...
验证码 换一张
取 消