Обновление
Хорошо, изначально я сказал, что «компилятор добавляет приведение к циклу 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
во время компиляции. Затем происходит следующее:
- Если тип равен типу локальной переменной (
x
в приведенном выше примере), используется прямое присвоение.
- Если тип является преобразуемым в тип локальной переменной (под этим я подразумеваю допустимое приведение от типа
e.Current
к типу x
), приведение вставлена.
- В противном случае компилятор выдаст ошибку.
Таким образом, в сценарии перечисления по 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
.