Итерационная переменная другого типа, чем коллекция? - PullRequest
7 голосов
/ 14 сентября 2010

У меня есть коллекция пустых значений.

Почему компилятор позволяет перебирать переменную типа int , а не 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
        }

Конечно, я должен обратить внимание на то, что я повторяю, но было бы неплохо, если бы компилятор сделал мне выговор :) Как здесь:

        foreach (bool i in collection)
        {
          // do sth 
        }

       //Error 1 Cannot convert type 'int' to 'bool'

Ответы [ 3 ]

4 голосов
/ 14 сентября 2010

Поскольку компилятор C # разыменовывает для вас Nullable<T>.

Если вы напишите этот код:

        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());
        }

Компилятор C # выдает:

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());
}

Обратите внимание на явное разыменование Nullable<int>.Value. Это свидетельство того, насколько укоренилась структура Nullable<T> во время выполнения.

3 голосов
/ 14 сентября 2010

Поведение, которое вы наблюдаете, соответствует разделу 8.8.4 оператора foreach спецификации языка C #.Этот раздел определяет семантику оператора foreach следующим образом:

[...] Приведенные выше шаги в случае успеха однозначно создают тип коллекции C, тип перечислителя E итип элемента T.foreach оператор вида

foreach (V v in x) embedded-statement

затем расширяется до:

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

В соответствии с правилами, определенными в спецификации, в вашем примере тип коллекции будет List<int?>, тип перечислителя будет List<int?>.Enumerator, а тип элемента будет int?.

Если вы заполните эту информацию в приведенном выше фрагменте кода, вы увидите, что int? явно приведен к int путем вызова Nullable<T> Explicit Conversion (Nullable<T> to T).Реализация этого явного оператора приведения, как описано Кентом, просто возвращает свойство Nullable<T>.Value.

3 голосов
/ 14 сентября 2010

Обновление

Хорошо, изначально я сказал, что «компилятор добавляет приведение к циклу foreach». Это не строго точно: оно не будет всегда добавлять приведения. Вот что на самом деле происходит.

Прежде всего, когда у вас есть этот foreach цикл:

foreach (int x in collection)
{
}

... вот основная схема (в псевдо-C #) того, что создает компилятор:

int x;
[object] e;
try
{
    e = collection.GetEnumerator();
    while (e.MoveNext())
    {
        x = [cast if possible]e.Current;
    }
}
finally
{
    [dispose of e if necessary]
}

Что? Я слышу, как вы говорите. Что вы подразумеваете под [object]? "

Вот что я имею в виду. Цикл foreach на самом деле требует без интерфейса , что означает, что он на самом деле немного волшебен. Требуется только, чтобы тип перечисляемого объекта предоставлял метод GetEnumerator, который, в свою очередь, должен предоставлять экземпляр некоторого типа, который предоставляет свойства MoveNext и Current.

Итак, я написал [object], потому что тип e не обязательно должен быть реализацией IEnumerator<int> или даже IEnumerator - что также означает, что он не обязательно должен реализовывать IDisposable (отсюда [dispose if necessary] часть).

Часть кода, о которой мы заботимся, чтобы ответить на этот вопрос, - это часть, в которой я написал [cast if possible]. Ясно, что поскольку компилятору не требуется фактическая реализация IEnumerator<T> или IEnumerator, тип e.Current не может считаться T, object или чем-то промежуточным. Вместо этого компилятор определяет тип e.Current на основе типа, возвращаемого GetEnumerator во время компиляции. Затем происходит следующее:

  1. Если тип равен типу локальной переменной (x в приведенном выше примере), используется прямое присвоение.
  2. Если тип является преобразуемым в тип локальной переменной (под этим я подразумеваю допустимое приведение от типа e.Current к типу x), приведение вставлена.
  3. В противном случае компилятор выдаст ошибку.

Таким образом, в сценарии перечисления по List<int?> мы переходим к шагу 2, и компилятор видит, что свойство Current типа List<int?>.Enumerator имеет тип int?, который может быть явно приведен к int .

Таким образом, строка может быть скомпилирована в эквивалент этого:

x = (int)e.Current;

Теперь, как выглядит оператор explicit для Nullable<int>? ​​

По рефлектору:

public static explicit operator T(T? value)
{
    return value.Value;
}

Итак, поведение, описанное Кентом , насколько я могу судить, просто оптимизация компилятора: явное приведение (int)e.Current является встроенным.

В качестве общего ответа на ваш вопрос я придерживаюсь своего утверждения о том, что компилятор вставляет приведения в цикл foreach, где это необходимо.


Оригинальный ответ

Компилятор автоматически вставляет приведенные значения при необходимости в цикл foreach по той простой причине, что до генериков не было интерфейса IEnumerable<T>, только IEnumerable*. Интерфейс IEnumerable предоставляет IEnumerator, который, в свою очередь, предоставляет доступ к свойству Current типа object.

Поэтому, если бы компилятор не выполнил приведение для вас, в старину, единственный способ, которым вы могли бы использовать foreach, был бы с локальной переменной типа object, которая, очевидно, могла бы отстой.

* И на самом деле, foreach вообще не требует никакого интерфейса - только метод GetEnumerator и сопутствующий тип с MoveNext и Current.

...