Почему коллекции 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 фактически генерируется как поле только для чтения, и передача, которая гарантирует, что мутации происходят на копии, берет верх.

к сожалению, спецификация дает мало указаний по этому вопросу. Очевидно, что-то сломано, потому что мы делаем это непоследовательно, но что право что делать, совсем не ясно.


методы структуры встроены, когда тип структуры известен во время компиляции, а метод вызова через интерфейс медленный, поэтому ответ: из-за причины производительности.