Все мы знаем, что изменяемые структуры являются злыми в целом. Я также вполне уверен это потому что IEnumerable
тип возвратов IEnumerator
, структуры сразу упаковываются в ссылочный тип, стоя больше, чем если бы они были просто ссылочными типами для начала.
Итак, почему в универсальных наборах BCL являются все перечислители изменяемыми структурами? Конечно, должно быть, было серьезное основание. Единственная вещь, которая происходит со мной, состоит в том, что структуры могут быть скопированы легко, таким образом сохранив состояние перечислителя в произвольной точке. Но добавление a Copy()
метод к IEnumerator
интерфейс был бы менее неприятным, таким образом, я не вижу это как являющееся логическим выравниванием самостоятельно.
Даже если бы я не соглашаюсь с проектным решением, я хотел бы смочь понять обоснование позади него.
Действительно, из соображений производительности. Команда 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 внутри блока using переместит перечислитель к следующему, независимо от того, является ли он структура или тип ссылки.
К сожалению, в сегодняшнем компиляторе C # есть ошибка. Если вы попали в такую ситуацию, мы непоследовательно выбираем, какой стратегии следовать.Сегодняшнее поведение таково:
если типизированная переменная, изменяемая с помощью метода, является нормальной локальной, тогда она обычно изменяется
, но если это поднятая локальная переменная (потому что это закрытая переменная анонимной функции или в блоке итератора), тогда локальный - это , фактически сгенерированный как поле только для чтения, и механизм, который гарантирует, что мутации произойдут в копии, вступает во владение.
К сожалению, в спецификации мало указаний по этому поводу. Ясно, что что-то не работает, потому что мы делаем это непоследовательно, но что делать правильное , совсем не ясно.
Методы структуры встроены, если тип структуры известен во время компиляции, а метод вызова через интерфейс медленный, поэтому ответ: из-за причины производительности.