Почему все нормально, что эта структура изменчива? Когда допустимы изменчивые структуры?

Эрик Липперт сказал мне, что я должен "стараться всегда делать типы значений неизменяемыми" , поэтому я решил, что должен стараться всегда делать типы значений неизменяемыми.

Но я только что нашел эту внутреннюю мутирующую структуру, System.Web.Util.SimpleBitVector32, в сборке System.Web, которая заставляет меня думать, что должна быть веская причина иметь изменяемую структуру. Я предполагаю, что они сделали это таким образом, потому что он работал лучше под тестированием, и они сохранили его внутреннее, чтобы препятствовать его неправильному использованию. Впрочем, это спекуляция.

У меня есть C & P'd источник этой структуры. Что оправдывает конструктивное решение использовать изменчивую структуру? В целом, какие выгоды могут быть достигнуты при подходе и когда эти преимущества достаточно значительны, чтобы оправдать потенциальные недостатки?

[Serializable, StructLayout(LayoutKind.Sequential)]
internal struct SimpleBitVector32
{
    private int data;
    internal SimpleBitVector32(int data)
    {
        this.data = data;
    }

    internal int IntegerValue
    {
        get { return this.data; }
        set { this.data = value; }
    }

    internal bool this[int bit]
    {
        get { 
            return ((this.data & bit) == bit); 
        }
        set {
            int data = this.data;
            if (value) this.data = data | bit;
            else this.data = data & ~bit;
        }
    }

    internal int this[int mask, int offset]
    {
        get { return ((this.data & mask) >> offset); }
        set { this.data = (this.data & ~mask) | (value << offset); }
    }

    internal void Set(int bit)
    {
        this.data |= bit;
    }

    internal void Clear(int bit)
    {
        this.data &= ~bit;
    }
}

Ответ 1

Собственно, если вы ищете все классы, содержащие BitVector в .NET framework, вы найдете кучу этих зверей: -)

  • System.Collections.Specialized.BitVector32 (единственный публичный...)
  • System.Web.Util.SafeBitVector32 (безопасный поток)
  • System.Web.Util.SimpleBitVector32
  • System.Runtime.Caching.SafeBitVector32 (безопасный поток)
  • System.Configuration.SafeBitVector32 (безопасный поток)
  • System.Configuration.SimpleBitVector32

И если вы посмотрите здесь, то они находятся в источнике SSCLI (Microsoft Shared Source CLI, aka ROTOR) System.Configuration.SimpleBitVector32, вы найдете этот комментарий:

//
// This is a cut down copy of System.Collections.Specialized.BitVector32. The
// reason this is here is because it is used rather intensively by Control and
// WebControl. As a result, being able to inline this operations results in a
// measurable performance gain, at the expense of some maintainability.
//
[Serializable()]
internal struct SimpleBitVector32

Я считаю, что это все говорит. Я думаю, что System.Web.Util один более сложный, но построенный на тех же основаниях.

Ответ 2

Учитывая, что полезная нагрузка представляет собой 32-разрядное целое число, я бы сказал, что это может быть легко написано как неизменяемая структура, возможно, без влияния на производительность. Если вы вызываете метод мутатора, который изменяет значение 32-битного поля или заменяет 32-разрядную структуру новой 32-разрядной структурой, вы все равно выполняете то же самое, что и в операциях с памятью.

Вероятно, кто-то хотел что-то, что было похоже на массив (хотя на самом деле это всего лишь биты в 32-разрядном целое), поэтому они решили, что хотят использовать синтаксис индексатора с ним, а не менее очевидный .WithTheseBitsChanged() метод, который возвращает новую структуру. Поскольку он не будет использоваться напрямую кем-либо, кроме веб-команды MS, и, возможно, не очень многими людьми даже в веб-команде, я полагаю, что у них было немного больше свободы в дизайнерских решениях, чем у людей, создающих публичные API.

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

Если вы ищете рекомендации по дизайну, я бы не потратил слишком много времени на поиск кода, который не был отполирован для общественного потребления.

Ответ 3

SimpleBitVector32 является изменчивым, я подозреваю, по тем же причинам, что BitVector32 является изменяемым. На мой взгляд, неизменное руководство - это просто рекомендация; однако для этого нужно иметь действительно хорошую причину.

Считайте также, что Dictionary<TKey, TValue> - я перейду в некоторые дополнительные подробности здесь. Словарь Entry struct изменен - ​​вы можете изменить TValue в любое время. Но Entry логически представляет значение.

Мутируемость должна иметь смысл. Я согласен с @JoeWhite: кто-то хотел что-то, что было похоже на массив (хотя на самом деле это всего лишь биты в 32-битном целое); также, что обе структуры BitVector могли бы быть... неизменными.

Но, как заявка на одеяло, я не согласен с этим, вероятно, был лишь некоторым личным предпочтением программиста в стиле кодирования и больше склонялся к тому, чтобы никогда не было [и нет] каких-либо веских оснований для его изменения. Просто знайте и понимайте ответственность за использование изменяемой структуры.

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

Обновление
Я не был уверен в своей оценке производительности при рассмотрении измененного типа значений v. Immutable. Однако, как отмечает @David, Эрик Липперт пишет это:

Бывают случаи, когда вам нужно выкручивать каждый последний бит производительности из системы. И в этих сценариях вам иногда приходится компромисс между чистым кодом, чистым, надежным, понятный, предсказуемый, модифицируемый и код, который не является ни одним из выше, но невероятно быстро.

Я выделил чистый, потому что изменяемая структура не соответствует чистому идеалу, что структура должна быть неизменной. Есть побочные эффекты написания изменчивой структуры: подражательность и предсказуемость скомпрометированы, поскольку Эрик продолжает объяснять:

Переменные типы значений... behave таким образом, что многие люди находят глубоко противоречащие друг другу, и тем самым упростить запись багги-кода (или правильный код, который легко превратился в ошибочный код случайно.) Но да, они очень быстры.

Точка, которую Эрик делает, заключается в том, что вы, как разработчик и/или разработчик, должны принять осознанное и информированное решение. Как вы узнаете? Эрик объясняет это также:

Я бы рассмотрел кодирование двух эталонных решений - один из них mutable structs, один с использованием неизменяемых структур - и запускать некоторые реалистичные ориентированные на пользователя сценарии. Но вот что: не выбирайте быстрее. Вместо этого решите, ПЕРЕД тем, как вы запустите тест как медленный неприемлемо медленный.

Мы знаем, что изменение типа значения быстрее, чем создание нового типа значения; но с учетом правильности:

Если оба решения приемлемы, выберите тот, который чист, правильно и достаточно быстро.

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

Ответ 4

Использование структуры для 32- или 64-битного вектора, как показано здесь, является разумным, с несколькими оговорками:

  • Я бы рекомендовал использовать цикл Interlocked.CompareExchange при выполнении любых обновлений структуры, а не просто использовать обычные булевы операторы напрямую. Если один поток пытается записать бит 3, а другой пытается записать бит 8, ни одна из них не должна мешать другому, задерживая его немного. Использование цикла Interlocked.CompareExchange предотвратит возможность ошибочного поведения (поток 1 считывает значение, поток 2 читает старое значение, поток 1 записывает новое значение, поток 2 записывает значение, вычисленное на основе старого значения, и отменяет изменение потока 1), не требуя каких-либо другой тип блокировки.
  • Следует избегать элементов структуры, кроме настроек свойств, которые изменяют "this". Лучше использовать статический метод, который принимает структуру в качестве эталонного параметра. Вызов элемента структуры, который модифицирует "this", обычно идентичен вызову статического метода, который принимает элемент как ссылочный параметр, как с точки семантики, так и с точки зрения производительности, но есть одно ключевое различие: если кто-то пытается передать структуру только для чтения ссылаясь на статический метод, вы получите ошибку компилятора. В отличие от этого, если вы вызываете в структуре только для чтения метод, который модифицирует "this", не будет никакой ошибки компилятора, но предполагаемая модификация не произойдет. Так как даже измененные структуры могут обрабатываться как доступные только для чтения в определенных контекстах, гораздо лучше получить ошибку компилятора, когда это произойдет, чем иметь код, который будет компилироваться, но не будет работать.

Эрик Липперт любит рассказывать о том, как изменчивые структуры злы, но важно признать его отношение к ним: он один из людей в команде С#, которому поручено создавать функции поддержки языка, такие как закрытие и итераторы. Из-за некоторых дизайнерских решений на раннем этапе создания .net правильная поддержка семантики значения-типа сложна в некоторых контекстах; его работа была бы намного проще, если бы не было каких-либо изменяемых типов значений. Я не жалею Эрика за его точку зрения, но важно отметить, что некоторые принципы, которые могут иметь важное значение в структуре и языке, не так применимы к дизайну приложения.

Ответ 5

Если я правильно понимаю, вы не можете сделать сериализуемую неизменяемую структуру просто используя SerializableAttribute. Это связано с тем, что во время десериализации сериализатор создает экземпляр по умолчанию для структуры, а затем устанавливает все поля после создания экземпляра. Если они выполняются только для чтения, десериализация завершится неудачно.

Таким образом, структура была изменчивой, иначе потребовалась бы сложная система сериализации.