Действительно, это из соображений производительности. Команда BCL провела лот исследований по этому вопросу, прежде чем принять решение о том, что вы по праву называете подозрительной и опасной практикой: использованием изменяемого типа значения.
Вы спрашиваете, почему это не вызывает бокс. Это потому, что компилятор C # не генерирует код для упаковки содержимого в IEnumerable или IEnumerator в цикле foreach, если он может этого избежать!
Когда мы видим
foreach(X x in c)
первое, что мы делаем, это проверяем, есть ли в c метод GetEnumerator. Если это так, то мы проверяем, имеет ли возвращаемый тип метод MoveNext и свойство current. Если это так, тогда цикл foreach генерируется полностью с использованием прямых вызовов этих методов и свойств. Только если «шаблон» не может быть сопоставлен, мы возвращаемся к поиску интерфейсов.
Это имеет два желательных эффекта.
Во-первых, если коллекция, скажем, является коллекцией целых, но была написана до того, как были изобретены универсальные типы, то она не берет штраф за упаковку значения Current для объекта, а затем распаковывает его для int. Если Current является свойством, которое возвращает int, мы просто используем его.
Во-вторых, если перечислитель является типом значения, он не переносит перечислитель в IEnumerator.
Как я уже сказал, команда BCL провела много исследований по этому вопросу и обнаружила, что в подавляющем большинстве случаев наказание за выделение и освобождение перечислителя было достаточно большим, чтобы его стоило сделать. тип значения, хотя это может привести к сумасшедшим ошибкам.
Например, рассмотрим это:
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h = somethingElse;
}
Вы вполне могли бы ожидать, что попытка изменить h окажется неудачной, и это действительно так. Компилятор обнаруживает, что вы пытаетесь изменить значение чего-либо, что находится в состоянии ожидания, и что это может привести к тому, что объект, который необходимо удалить, фактически не будет удален.
Теперь предположим, что у вас было:
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h.Mutate();
}
Что здесь происходит? Можно разумно ожидать, что компилятор будет делать то же, что и он, если бы h было полем только для чтения: делает копию и изменяет копию , чтобы метод не выбрасывал вещи в нужное значение быть утилизированным.
Однако это противоречит нашей интуиции о том, что должно происходить здесь:
using (Enumerator enumtor = whatever)
{
...
enumtor.MoveNext();
...
}
Мы ожидаем, что выполнение MoveNext внутри используемого блока будет перемещать перечислитель к следующему независимо от того, является ли он структурой или типом ссылки.
К сожалению, сегодня в компиляторе C # есть ошибка. Если вы находитесь в такой ситуации, мы выбираем, какой стратегии следовать непоследовательно. Поведение сегодня:
если переменная с типом значения, мутирующая с помощью метода, является нормальной локальной, тогда она мутирует нормально
но если это локальный локальный объект (поскольку это закрытая переменная анонимной функции или в блоке итератора), то локальный является , фактически сгенерированным как поле только для чтения, и механизм, который гарантирует, что мутации происходят в копии, вступает во владение.
К сожалению, в спецификации мало указаний по этому вопросу. Ясно, что что-то сломано, потому что мы делаем это непоследовательно, но то, что нужно сделать правильно , не совсем понятно.