Почему коллекции BCL используют структурные перечислители, а не классы? - PullRequest
53 голосов
/ 02 июля 2010

Мы все знаем, изменчивые структуры являются злом в целом. Я также вполне уверен, что поскольку IEnumerable<T>.GetEnumerator() возвращает тип IEnumerator<T>, структуры сразу помещаются в ссылочный тип, что стоит больше, чем если бы они были просто ссылочными типами для начала.

Так почему же в универсальных коллекциях BCL все перечислители являются изменяемыми структурами? Конечно, должна была быть веская причина. Единственное, что приходит мне в голову, - это то, что структуры могут быть легко скопированы, таким образом сохраняя состояние счетчика в произвольной точке. Но добавление метода Copy() к интерфейсу IEnumerator было бы менее проблематичным, поэтому я не вижу в этом логического оправдания.

Даже если я не согласен с дизайнерским решением, я хотел бы понять причины этого.

Ответы [ 2 ]

76 голосов
/ 02 июля 2010

Действительно, это из соображений производительности. Команда 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 # есть ошибка. Если вы находитесь в такой ситуации, мы выбираем, какой стратегии следовать непоследовательно. Поведение сегодня:

  • если переменная с типом значения, мутирующая с помощью метода, является нормальной локальной, тогда она мутирует нормально

  • но если это локальный локальный объект (поскольку это закрытая переменная анонимной функции или в блоке итератора), то локальный является , фактически сгенерированным как поле только для чтения, и механизм, который гарантирует, что мутации происходят в копии, вступает во владение.

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

6 голосов
/ 02 июля 2010

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

...