Почему Наборы BCL используют перечислители структуры, не классы?

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

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

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

53
задан Cody Gray 18 September 2014 в 16:00
поделиться

2 ответа

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

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

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

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

76
ответ дан 7 November 2019 в 08:46
поделиться

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

6
ответ дан 7 November 2019 в 08:46
поделиться
Другие вопросы по тегам:

Похожие вопросы: