Почему коллекции BCL используют перечислители структуры, а не классы?
мы все знаем изменчивые структуры-зло в целом. Я также уверен, что потому что IEnumerable<T>.GetEnumerator()
возвращает значение типа IEnumerator<T>
, структуры немедленно упаковываются в ссылочный тип, стоящий больше, чем если бы они были просто ссылочными типами для начала.
так почему же в общих коллекциях BCL все перечислители являются изменяемыми структурами? Наверняка на то была веская причина. Единственное, что мне приходит в голову, - это то, что структуры можно легко скопировать, сохранив состояние перечислителя в произвольной точке. Но добавление Copy()
метод IEnumerator
интерфейс был бы менее хлопотным, поэтому я не вижу в этом логического оправдания самостоятельно.
даже если я не согласен с проектным решением, я хотел бы быть в состоянии понять причины этого.
2 ответов
действительно, это по соображениям производительности. Команда BCL сделала много исследования по этому вопросу, прежде чем принять решение пойти с тем, что вы справедливо называете подозрительной и опасной практикой: использование изменяемого типа значения.
вы спрашиваете, почему это не вызывает бокс. Это потому, что компилятор C# не генерирует код для коробки вещей в IEnumerable или IEnumerator в цикле foreach, если он может этого избежать!
когда мы видим
foreach(X x in c)
первый мы проверяем, есть ли у c метод GetEnumerator. Если это так,то мы проверяем, имеет ли возвращаемый тип метод MoveNext и свойство current. Если это так, то цикл foreach генерируется полностью с использованием прямых вызовов этих методов и свойств. Только если "шаблон" не может быть сопоставлен, мы возвращаемся к поиску интерфейсов.
это имеет два желательных эффекта.
во-первых, если коллекция, скажем, коллекция ints, но была написано до того, как были изобретены общие типы, тогда он не принимает боксерский штраф бокса значение тока для объекта, а затем распаковывает его в 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 внутри использование блока будет переместите перечислитель на следующий, независимо от того, является ли он структурой или типом ref.
к сожалению, компилятор C# сегодня имеет ошибку. Если вы находитесь в этой ситуации, мы выбираем какой стратегии следовать непоследовательно. Поведение сегодня:
если переменная со значением, изменяемая с помощью метода, является нормальной локальной, то она мутирует нормально
но если это поднятый локальный (потому что это закрытая переменная анонимной функции или в блоке итератора), то локальный is фактически генерируется как поле только для чтения, и передача, которая гарантирует, что мутации происходят на копии, берет верх.
к сожалению, спецификация дает мало указаний по этому вопросу. Очевидно, что-то сломано, потому что мы делаем это непоследовательно, но что право что делать, совсем не ясно.
методы структуры встроены, когда тип структуры известен во время компиляции, а метод вызова через интерфейс медленный, поэтому ответ: из-за причины производительности.