Создано при помощи нейросети Kandinsky

Класс AinByteValue на C# для управления битами внутри байта

User Rating: 0 / 5

Изображение для статьи создано при помощи нейросети Kandinsky

Доброго времени суток, друзья.

В этой статье я хотел бы рассмотреть вариант класса для C#, который я назвал AinByteValue. Смысл данного класса - предоставить доступ разработчику C# до конкретных битов внутри значения типа byte и дать возможность управлять этими битами при помощи открытых методов/свойств, которые будет предоставлять класс, а также быстро получать значение конкретного бита в заданной позиции. Признаюсь, меня давно интересовала тема того, каким образом можно оперировать отдельно взятыми битами внутри одного байта и использовать эти отдельно взятые биты в качестве "флажков", которые могли бы быть аналогом типа данных bool, но занимать меньше памяти программы.

Возникает закономерный вопрос: а для чего и где этот класс может понадобиться в принципе при разработке программ на C#? Зачем нам вообще управлять битами внутри байта? Ведь в языке C# и так есть уже множество стандартных типов данных, таких как bool, byte, int и т. д.

Но давайте представим ситуацию, что мы пишем некоторую программу и проектируем в ней класс, где нам требуется хранить определённое количество "флажков" булева типа (bool). Каждый из таких флажков несёт за собой какую-то функциональность класса и связанную с ней логику. Например, класс мог бы выглядеть так:

    public class TestClassWithBooleans {
        private bool myFlag1;
        private bool myFlag2;
        private bool myFlag3;
        private bool myFlag4;
        private bool myFlag5;
        private bool myFlag6;
        private bool myFlag7;
        private bool myFlag8;

        public TestClassWithBooleans() {
            // здесь какой-то код конструктора
        }

        // здесь какие-то методы класса, использующие фнутренние флаги myFlag1, ..., myFlag8
    }

 

Здесь для простоты названия всех флажков я придумал "из головы", и они тривиальны: все с префиксом 'myFlag' и индексом от 1 до 8 в конце каждого имени поля класса. В реальной же программе, где потребуется множество переменных типа bool внутри класса, конечно, им были бы даны какие-то более читаемые и осмысленные имена, и была бы реализована соответствующая логика для работы с каждым из флажков.

При взгляде на этот класс с множеством флажков (а их всего 8 в примере выше) у меня возникает вопрос: а что если вместо 8-ми отдельных полей с типом bool я мог бы использовать одно поле типа данных byte и уметь управлять каждым из 8-ми его битов по отдельности? При этом каждый бит и является нужным мне флажком и также несёт за собой какую-то функциональность? Вопрос, думаю, становится ещё актуальнее в плане использования памяти при выполнении программы, если я буду создавать большое или очень большое количество объектов класса TestClassWithBooleans. Снова сразу приходит мысль о том, что в подобной ситуации, если бы я мог хранить в классе все 8 признаков внутри одного байта а не в разных полях с типами bool, то экономия по используемой памяти была бы существенно больше.

И другой момент, который мне также всегда хотелось глубже освоить на практике с языком C# (но увы, как-то не приходилось) - это использование операций побитового сдвига (<<, >>) и бинарных операторов - побитового "И", "ИЛИ". Подробнее про операторы, которые я имею в виду, можно узнать по следующим ссылкам:

В контексте моей экспериментальной задачи - предоставить доступ к отдельным битам внутри байта и управлять ими - я пришёл к выводу, что я также смогу задействовать эти операторы при написании своего класса AinByteValue.

Давайте по шагам пройдем путь, который прошёл я при написании этого класса.

Сначала мы создадим класс AinByteValue в отдельном файле класса AinByteValue.cs внутри нового пустого проекта. В моём случае я предварительно создал в среде Microsoft Visual Studio проект с типом "Библиотека классов (Майкрософт)" и в него добавил этот класс. Изначально мы хотим, чтобы класс AinByteValue содержал основное (и единственное) открытое свойство Value с типом данных byte и пару конструкторов - один без параметров, а второй пусть принимает исходное значение, если мы захотим сразу инициализировать экземпляр класса каким-то заготовленным значением типа byte:

    public class AinByteValue {
        /// <summary>
        /// Возвращает или устанавливает внутреннее значение (имеющее тип byte) для данного экземпляра AinByteValue
        /// </summary>
        public byte Value { get; set; }

        /// <summary>
        /// Конструктор без параметров. Создаст экземпляр AinByteValue, где биты во всех позициях установлены в 0
        /// </summary>
        public AinByteValue() {
            Value = 0;
        }

        /// <summary>
        /// Конструктор с параметрами. Создаст экезмпляр AinByteValue по заданному параметру <paramref name="value"/>
        /// </summary>
        /// <param name="value">значение параметра определит внутреннее значение для данного экземпляра AinByteValue</param>
        public AinByteValue(byte value) {
            Value = value;
        }
    }

 

Прекрасно, теперь мы уже можем создавать экземпляры нашего нового класса, например, так:

// создаст экземпляр класса, где внутреннее значение байта будет равно 0,
// а значит, все его биты также равны 0
AinByteValue myByteValue = new AinByteValue();

// создаём другой экземпляр и инициализируем внутреннее открытое свойство 
// Value значением 128
AinByteValue myAnotherByteValue = new AinByteValue(128);

 

Теперь мы хотим приблизиться к основной цели проектируемого класса: как нам получать доступ и управлять отдельно взятыми битами для внутреннего свойства Value, имеющего тип byte?

Давайте начнём с простого: научимся сперва получать значение бита, находящегося в нулевой позиции, из свойства Value нашего класса. Здесь я чуть поясню внутреннее "устройство" типа данных byte (опытные разработчики могут пропустить эту часть): он может принимать значения лишь от 0 до 255, это границы типа данных, т.е. при попытке присвоить какой-то переменной с типом byte отрицательное значение или значение, превышающее 255, мы получим ошибку компиляции:

byte invalidByte1 = -1; // будет вызвана ошибка компиляции: 'Значение константы "-1" не может быть преобразовано в "byte"'

byte invalidByte2 = 256; // аналогично, та же ошибка компиляции: 'Значение константы "256" не может быть преобразовано в "byte"'

byte validByte3 = 0; // так можно, минимальное значение для типа данных byte
byte validByte4 = 122; // так тоже можно
byte validByte5 = 255; // и так можно, это граничное верхнее значение для типа данных byte

 

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

Минимальное значение для байта, равное 0, в битовом/бинарном представлении выглядит так:

Позиция бита внутри байта: 7-я 6-я 5-я 4-я 3-я 2-я 1-я 0-я
Значение бита в позиции: 0 0 0 0 0 0 0 0

 

Как видим, биты во всех восьми позициях (0 - крайняя правая позиция, 7 - крайняя левая позиция) равны, ожидаемо нулю.

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

Позиция бита внутри байта: 7-я 6-я 5-я 4-я 3-я 2-я 1-я 0-я
Значение бита в позиции: 1 1 1 1 1 1 1 1

 

Т.е. биты во всех позициях внутри байта теперь выставлены в единицу.

Давайте убедимся, что переводе из двоичной системы счисления в десятичную запись 11111111 в двоичном виде эквивалентна значению 255 в десятичном виде. Для этого нужно справа-налево умножить значения каждого бита на 2 в степени, равной позиции этого бита, то есть:

1 × 20 + 1 × 21 + 1 * 22 + 1 × 23 + 1 × 24 + 1 × 25 + 1 × 26 + 1 × 27 = 1 × 1 + 1 × 2 + 1 × 4 + 1 × 8 + 1 × 16 + 1 × 32 + 1 × 64 = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255

Точно таким же образом несложно вычислить и любое другое число "помещаемое" в допустимых граничных значениях для байта в двоичной системе счисления и перевести его в десятичную систему счисления. Например, так внутри байта хранится десятичное число 7:

Позиция бита внутри байта: 7-я 6-я 5-я 4-я 3-я 2-я 1-я 0-я
Значение бита в позиции: 0 0 0 0 0 1 1 1

 

1 × 20 + 1 × 21 + 1 * 22 + 0 × 23 + 0 × 24 + 0 × 25 + 0 × 26 + 0 × 27 = 1 × 1 + 1 × 2 + 1 × 4 = 1 + 2 + 4 = 7

Итак, вернёмся к нашему классу AinByteValue и научимся теперь получать бит в нулевой позиции из свойства Value. Для этого добавим в наш класс два открытых (public) свойства Bit0 (тип данных int) и IsBit0 (тип данных bool):

        /// <summary>
        /// Возвращает значение бита в позиции 0
        /// </summary>
        public int Bit0 {
            get {
                return Value & 0b0000_0001;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 0 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit0 {
            get => Bit0 == 1;
        }

 

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

Запись, используемая для получения значения свойства,

return Value & 0b0000_0001;

 

производит побитовую операцию "И" между текущим значением Value и константой 0b0000_0001. Эта константа является литералом, представляющим собой запись целого числа 1 в бинарном виде и эквивалентного числу 1 в десятичной системе счисления.

Побитовая операция "И" (используемый оператор &) фактически сделает следующее: для возвращаемого свойством Bit0 результирующего значения она "сбросит" абсолютно все биты в позициях с 1-й по 7-ю, кроме 0-й позиции. Для нулевой позиции результирующий бит будет зависеть от того, как он выставлен в самом внутреннем свойстве Value, хранимом в экземпляре класса. То есть если бит в 0-й позиции внутри Value равен 1, то в результате Bit0 вернёт значение 1. Если же бит в 0-й позиции внутри Value равен 0, то в результате Bit0 вернёт значение 0. Все биты с 1-й по 7-ю позицию мы сбрасываем для того, чтобы свойство Bit0 гарантированно возвращало либо 1, либо 0.

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

Аналогичным образом мы добавим в класс пары свойств BitN и IsBitN для всех возможных позиций битов внутри байта. Получим следующее:

       /// <summary>
        /// Возвращает значение бита в позиции 0
        /// </summary>
        public int Bit0 {
            get {
                return Value & 0b0000_0001;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 0 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit0 {
            get => Bit0 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 1
        /// </summary>
        public int Bit1 {
            get {
                return (Value & 0b0000_0010) >> 1;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 1 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit1 {
            get => Bit1 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 2
        /// </summary>
        public int Bit2 {
            get {
                return (Value & 0b00000100) >> 2;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 2 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit2 {
            get => Bit2 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 3
        /// </summary>
        public int Bit3 {
            get {
                return (Value & 0b00001000) >> 3;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 3 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit3 {
            get => Bit3 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 4
        /// </summary>
        public int Bit4 {
            get {
                return (Value & 0b00010000) >> 4;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 4 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit4 {
            get => Bit4 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 5
        /// </summary>
        public int Bit5 {
            get {
                return (Value & 0b00100000) >> 5;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 5 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit5 {
            get => Bit5 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 6
        /// </summary>
        public int Bit6 {
            get {
                return (Value & 0b01000000) >> 6;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 6 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit6 {
            get => Bit6 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 7
        /// </summary>
        public int Bit7 {
            get {
                return (Value & 0b10000000) >> 7;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 7 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit7 {
            get => Bit7 == 1;
        }

 

Вы можете обратить внимание, что если для получения бита в 0-й позиции мы не использовали операторы побитового сдвига, то для всех других позиций битов мы используем побитовый сдвиг. Например, для получения значения свойства Bit1 используем побитовый сдвиг вправо на 1 позицию, при этом операцию побитового "И" мы выполняем предварительно в круглых скобках, повышая ей приоритет (т.е. она будет выполнена первой):

return (Value & 0b0000_0010) >> 1;

 

Для чего это нужно? Всё довольно просто и не сильно отличается от получения значения бита в 0-й позиции, рассмотренного нами выше. Дело в том, что операция побитового сдвига вправо нам помогает в прямом смысле "сдвинуть" результирующий бит после проведения операции побитового "И" вправо на нужное количество позиций, при этом во время сдвига биты в левых позициях (7-я в нашем случае) заполнятся нулями.

Поскольку каждое свойство BitN всегда должно возвращать гарантированно либо 0, либо 1 (ведь бы хотим понять установлен ли бит или нет в заданной позиции), то в случае, если мы не сделаем побитовый сдвиг вправо, то на выходе для свойства Bit1 мы получим два возможных варианта:

  • если бит в 1-й позиции внутри Value установлен, вернётся 0b000_0010, т.е. значение 2, вместо нужного нам значения 1!
  • если бит в 1-й позиции внутри Value не установлен, вернётся 0b000_0000, т.е. значение 0

Поэтому для каждого из свойств BitN мы делаем побитовый сдвиг вправо на то количество позиций, которое равно значению позиции интересующего нас бита внутри Value.

Можно сказать, что мы на данном шаге написали большую часть функциональности нашего класса AinByteValue. Теперь мы можем постепенно "учить" наш класс полезным методам.

Например, давайте добавим к классу быстрые методы для сброса всех битов в 0 и установки всех битов в 1. Для этого не нужно никаких "хитростей", ведь "под капотом" эти методы просто должны сбросить свойство Value класса в 0 (или 0b0000_0000 в двоичной системе) или же установить его значение в 255 (что эквивалентно 0b1111_1111 в двоичной системе, т.е. все биты установлены):

        /// <summary>
        /// Сбрасывает все биты во всех позициях (от 0 до 7) в значение 0
        /// </summary>
        public void ResetAllBits() {
            Value = 0;
        }

        /// <summary>
        /// Устанавливает все биты во всех позициях (от 0 до 7) в значение 1
        /// </summary>
        public void SetAllBits() {
            Value = 255;
        }

 

Теперь нам нужно "научить" наш класс AinByteValue устанавливать биты в требуемых позициях (от 0 до 7) в нужные нам значения.

Опять же, начнём с простого - научимся устанавливать бит в нулевой позиции, для чего напишем соответствующий метод SetBit0(bool isSet). Он будет принимать на вход параметр isSet, если мы хотим выставить бит в единицу в нулевой позиции, передавать в параметр будем true, в противном случае false:

        /// <summary>
        /// Устанавливает бит в позиции 0 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 0: true - установить бит, false - сбросить бит</param>
        public void SetBit0(bool isSet) {            
            if (isSet) {
                Value = (byte)((Value & 0b1111_1110) + 1);
            } else {
                Value = (byte)(Value & 0b1111_1110);
            }
        }

 

Что мы делаем внутри метода:

  • если параметр isSet выставлен, мы применяем знакомую уже нам операцию побитового "И" к двум операндам: первый - это наше внутреннее представление байта, т.е. свойство Value, второй - это константное значение 0b1111_1110. Обратите внимание, что здесь в константе мы, наоборот (в отличие от механики для свойств Bit0..Bit7), выставили в единицу все биты в позициях с 1-й по 7-ю. Это нужно для того, чтобы "сохранить" и не потерять текущее значение, уже хранимое в Value. В результате те биты, которые и так выставлены в 1 в соответствующих позициях внутри Value останутся единицами, а те, что равны нулю, также на выходе останутся равными 0. Исключение - бит в 0-й позиции, здесь мы намеренно его "обнуляем", чтобы временно скинуть в результате для побитового "И", но тут же гарантированно "вернуть" при помощи прибавления единицы.
  • если же параметр isSet не выставлен, то это значит, что мы хотим вызовом метода SetBit0 "сбросить" бит в нулевой позиции, поэтому мы просто применяем операцию побитового "И", но без прибавления единицы.

Теперь, по сути, нам нужно добавить аналогичные методы SetBitN для всех позиций битов от 1-й до 7-й, т.к. SetBit0 мы уже реализовали выше.

Попробуем написать аналогичный метод SetBit1(bool isSet) для выставления бита в 1-й позиции:

        /// <summary>
        /// Устанавливает бит в позиции 0 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 0: true - установить бит, false - сбросить бит</param>
        public void SetBit1(bool isSet) {            
            if (isSet) {
                Value = (byte)((Value & 0b1111_1101) + 2);
            } else {
                Value = (byte)(Value & 0b1111_1101);
            }
        }

 

Нетрудно заметить основную разницу этого метода с вышеописанным методом SetBit0: теперь для побитового "И" мы используем другую константу - с нулём лишь в 1-й позиции. Также мы после операции побитового "И" прибавляем значение 2, вместо 1, как в методе SetBit0. Почему 2? Потому что 2 в двоичной записи выглядит как 0b0000_0010, и единица в 1-й позиции гарантированно установит бит в этой позиции в результирующем значении.

Легко догадаться, что для всех остальных методов SetBit2 ... SetBit7, которые нам предстоит также добавить в класс, нам придётся практически дублировать всю эту логику. Разница будет лишь в том, что 0 в константе будет "сдвигаться" влево на очередную позицию, а прибавлять мы будем следующую степень двойки, поскольку именно степень двойки в двоичной системе даёт нам нули во всех позициях, кроме нужной нам. Дабы избежать "копипаста" кода для всех методов, давайте напишем новый закрытый (private) метод класса с именем SetBitInternal(bool isSet, int bitwiseAndValue, byte increment), который будет принимать специфичные параметры для каждой возможной позиции устанавливаемого бита:

        /// <summary>
        /// Закрытый метод класса. Устанавливает заданный бит во внутреннем значении экземпляра, на основании
        /// переденных параметров.
        /// </summary>
        /// <param name="isSet">true - если значение бита нужно установить, false - если значение бита нужно сбросить</param>
        /// <param name="bitwiseAndValue">значение, определяющее бит и его позицию для установки и сброса; используется при операции побитового 'И' ('bitwise and') по отношению к внутреннему значению экземпляра AinByteValue</param>
        /// <param name="increment">определяет значение, которое необходимо прибавить к внутреннему значению экземпляра AinByteValue после осуществления операции побитового 'И' ('bitwise and')</param>
        private void SetBitInternal(bool isSet, int bitwiseAndValue, byte increment) {
            if (isSet) {
                Value = (byte)((Value & bitwiseAndValue) + increment);
            } else {
                Value = (byte)(Value & bitwiseAndValue);
            }
        }

 

Теперь мы можем существенно сократить код внутри ранее написанных методов SetBit0 и SetBit1. Достаточно лишь вызвать новый метод SetBitInternal, передавая ему нужные аргументы:

        public void SetBit0(bool isSet) {
            SetBitInternal(isSet, 0b1111_1110, 1);
        }

        public void SetBit1(bool isSet) {
            SetBitInternal(isSet, 0b1111_1101, 2);
        }

 

Теперь нам легко написать и все остальные методы для установки битов в позициях со 2-й по 7-ю:

        /// <summary>
        /// Устанавливает бит в позиции 2 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 2: true - установить бит, false - сбросить бит</param>
        public void SetBit2(bool isSet) {
            SetBitInternal(isSet, 0b1111_1011, 4);
        }

        /// <summary>
        /// Устанавливает бит в позиции 3 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 3: true - установить бит, false - сбросить бит</param>
        public void SetBit3(bool isSet) {
            SetBitInternal(isSet, 0b1111_0111, 8);
        }

        /// <summary>
        /// Устанавливает бит в позиции 4 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 4: true - установить бит, false - сбросить бит</param>
        public void SetBit4(bool isSet) {
            SetBitInternal(isSet, 0b1110_1111, 16);
        }

        /// <summary>
        /// Устанавливает бит в позиции 5 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 5: true - установить бит, false - сбросить бит</param>
        public void SetBit5(bool isSet) {
            SetBitInternal(isSet, 0b1101_1111, 32);
        }

        /// <summary>
        /// Устанавливает бит в позиции 6 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 6: true - установить бит, false - сбросить бит</param>
        public void SetBit6(bool isSet) {
            SetBitInternal(isSet, 0b1011_1111, 64);
        }

        /// <summary>
        /// Устанавливает бит в позиции 7 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 7: true - установить бит, false - сбросить бит</param>
        public void SetBit7(bool isSet) {
            SetBitInternal(isSet, 0b0111_1111, 128);
        }

 

Фактически, код класса готов, и мы можем уже его использовать и тестировать. Об этом чуть далее, а пока давайте добавим ещё один метод, который может пригодиться в том случае, когда мы захотим установить/сбросить биты в заданных позициях, делая это одним вызовом. Для этого будет необходимо перечислить позиции битов, которые мы хотим установить или сбросить. Назовём новый метод SetBits(bool isSet, params byte[] bitsIndices). Он будет возвращать всегда true, если индексы битов в bitsIndices были переданы верно (значения от 0 до 7) и будет возвращать false, если хотя бы один из индексов битов был передан некорректно:

        /// <summary>
        /// Устанавливает или сбрасывает биты в позициях, задаваемых параметром
        /// <paramref name="bitsIndices"/>.
        /// Если массив индексов битов содержит значения, находящиеся в корректных пределах
        /// (от 0 до 7), то функция установит биты в заданных позициях и вернёт true.
        /// В случае, если <paramref name="bitsIndices"/> содержит хотя бы один индекс бита,
        /// выходящий за допустимые пределы (от 0 до 7), функция вернёт false, однако установит
        /// требуемое значение для битов, значения индексов для которых были корректно переданы  
        /// в параметре <paramref name="bitsIndices"/> и находятся в допустимых пределах.
        /// </summary>
        /// <param name="isSet">значение для установки битов в заданных позициях: true - установить бит, false - сбросить бит</param>
        /// <param name="bitsIndices">массив, содержащий индексы тех битов, для которых требуется установить заданное значение</param>
        /// <returns></returns>
        public bool SetBits(bool isSet, params byte[] bitsIndices) {
            if (bitsIndices == null) {
                return false;
            }
            bool isError = false;
            foreach (byte bitIndex in bitsIndices) {
                switch (bitIndex) {
                    case 0:
                        SetBit0(isSet);
                        break;
                    case 1:
                        SetBit1(isSet);
                        break;
                    case 2:
                        SetBit2(isSet);
                        break;
                    case 3:
                        SetBit3(isSet);
                        break;
                    case 4:
                        SetBit4(isSet);
                        break;
                    case 5:
                        SetBit5(isSet);
                        break;
                    case 6:
                        SetBit6(isSet);
                        break;
                    case 7:
                        SetBit7(isSet);
                        break;
                    default:
                        isError = true;
                        break;
                }
            }
            return !isError;
        }

 

На этом наш класс написан, при желании можем также добавить в него для удобства статический метод From, который быстро создаст новый экземпляр класса на основе переданного значения с типом byte:

        /// <summary>
        /// Создаёт экземпляр AinByteValue из заданного значения <paramref name="value"/>
        /// </summary>
        /// <param name="value">значение, которым будет проинициализирован новый экземпляр AinByteValue при его создании</param>
        /// <returns></returns>
        public static AinByteValue From(byte value) {
            return new AinByteValue(value);
        }

 

Тестирование класса AinByteValue

Наш класс написан, но пока не ясно, работает он или нет. Давайте создадим отдельный проект с типом "Проект тестов MSTest" и добавим его к текущему решению в среде Microsoft Visual Studio. К зависимостям для этого проекта тестов нам необходимо будет добавить ссылку на ранее созданный проект, куда мы добавили наш класс AinByteValue, иначе проект тестов "не увидит" наш новый класс, и мы не сможем писать тесты на класс.

В проекте тестов создадим новый класс TestAinByteValueClassMethods, в котором разместим различные тестовые методы, проверяющие и заодно демонстрирующие примеры работы с только что написанным нами классом AinByteValue. Добавим сразу первый тестовый метод, который проверит, что при создании экземпляра класса AinByteValue со значением 3, биты в позициях 0 и 1 ожидаемо будут выставлены в 1, а остальные биты будут сброшены в 0:

    [TestClass]
    public class TestAinByteValueClassMethods {
        /// <summary>
        /// Тест проверяет, что при инициализации экземпляра AinByteValue значением 3, 
        /// биты в 0-й и 1-й позиции ожидаемо равны единице, а остальные биты - сброшены
        /// </summary>
        [TestMethod]
        public void TestByteValue3() {
            AinByteValue byteValue = new AinByteValue(3);

            Assert.IsTrue(byteValue.IsBit0);
            Assert.IsTrue(byteValue.IsBit1);
            Assert.IsFalse(byteValue.IsBit2);
            Assert.IsFalse(byteValue.IsBit3);
            Assert.IsFalse(byteValue.IsBit4);
            Assert.IsFalse(byteValue.IsBit5);
            Assert.IsFalse(byteValue.IsBit6);
            Assert.IsFalse(byteValue.IsBit7);
        }
    }

 

Напишем в классе TestAinByteValueClassMethods для тестовых методов аналогичный метод, который проверит на этот раз биты в случае инициализации экземпляра класса AinByteValue значением 2:

        /// <summary>
        /// Тест проверяет, что при инициализации экземпляра AinByteValue значением 2 
        /// лишь бит в позиции 1 равен единице, а остальные биты - сброшены
        /// </summary>
        [TestMethod]
        public void TestByteValue2() {
            AinByteValue byteValue = new AinByteValue(2);

            Assert.IsFalse(byteValue.IsBit0);
            Assert.IsTrue(byteValue.IsBit1);
            Assert.IsFalse(byteValue.IsBit2);
            Assert.IsFalse(byteValue.IsBit3);
            Assert.IsFalse(byteValue.IsBit4);
            Assert.IsFalse(byteValue.IsBit5);
            Assert.IsFalse(byteValue.IsBit6);
            Assert.IsFalse(byteValue.IsBit7);
        }

 

Напишем ещё один тест, на этот раз проверим, что если мы изначально создали экземпляр класса AinByteValue на базе значения 0 с типом byte, а затем вызвали у него SetBit1(true), то внутреннее значение свойства Value у экземпляра будет равно 2 (поскольку 2 - это 0b0000_0010 в двоичной системе, где выставлен лишь бит в 1-й позиции, что и подразумевает проверка тестом):

        /// <summary>
        /// Тест проверяет, что для экземпляра AinByteValue, исходно инициализированного
        /// значением 0, при вызове метода SetBit1 со значением true, результирующее
        /// внутреннее значение свойства станет равным 2
        /// </summary>
        [TestMethod]
        public void TestSetBit1Method() {
            AinByteValue byteValue = new AinByteValue(0);
            byteValue.SetBit1(true);
            Assert.AreEqual(2, byteValue.Value);
        }

 

Ещё один тест, который проверит, что при простановке всех битов результирующее значение будет равно 255:

        [TestMethod]
        public void TestAllBitsAreSet() {
            AinByteValue byteValue = new AinByteValue(0);
            byteValue.SetBit0(true);
            byteValue.SetBit1(true);
            byteValue.SetBit2(true);
            byteValue.SetBit3(true);
            byteValue.SetBit4(true);
            byteValue.SetBit5(true);
            byteValue.SetBit6(true);
            byteValue.SetBit7(true);
            Assert.AreEqual(255, byteValue.Value);
        }

 

Напишем ещё парочку тестов, которые проверят различные варианты вызовов метода SetBits:

        /// <summary>
        /// Тест проверит возвращаемое значение метода SetBits на равенство true в случае
        /// передачи валидных индексов для позиции устанавливаемых битов
        /// </summary>
        [TestMethod]
        public void TestSetBitIndicesMethodNoError() {
            AinByteValue byteValue = new AinByteValue();

            Assert.IsTrue(byteValue.SetBits(true, 0, 1, 2));
            Assert.AreEqual(7, byteValue.Value);         
        }

        /// <summary>
        /// Тест проверит возвращаемое значение метода SetBits на равенство false в случае
        /// передачи невалидного индекса (20) среди позиций для устанавливаемых битов
        /// </summary>
        [TestMethod]
        public void TestSetBitIndicesMethodWhenError() {
            AinByteValue byteValue = new AinByteValue();

            Assert.IsFalse(byteValue.SetBits(true, 0, 1, 2, 20));
            Assert.AreEqual(7, byteValue.Value);
        }

 

И ещё один вариант теста, проверяющий метод ResetAllBits у нашего класса:

        /// <summary>
        /// Тест проверит метод ResetAllBits и внутреннее значение свойства Value
        /// </summary>
        [TestMethod]
        public void TestResetAllBitsMethod() {
            AinByteValue byteValue = new AinByteValue(255);
            byteValue.ResetAllBits();
            Assert.AreEqual(0, byteValue.Value);
        }

Заключение. Полный исходный текст получившегося класса AinByteValue

На всякий случай я прилагаю полный исходный текст получившегося в рамках статьи класса AinByteValue вместе с комментариями к полям, свойствам и методам класса:

    public class AinByteValue {
        /// <summary>
        /// Возвращает или устанавливает внутреннее значение (имеющее тип byte) для данного экземпляра AinByteValue
        /// </summary>
        public byte Value { get; set; }

        /// <summary>
        /// Возвращает значение бита в позиции 0
        /// </summary>
        public int Bit0 {
            get {
                return Value & 0b0000_0001;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 0 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit0 {
            get => Bit0 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 1
        /// </summary>
        public int Bit1 {
            get {
                return (Value & 0b0000_0010) >> 1;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 1 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit1 {
            get => Bit1 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 2
        /// </summary>
        public int Bit2 {
            get {
                return (Value & 0b00000100) >> 2;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 2 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit2 {
            get => Bit2 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 3
        /// </summary>
        public int Bit3 {
            get {
                return (Value & 0b00001000) >> 3;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 3 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit3 {
            get => Bit3 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 4
        /// </summary>
        public int Bit4 {
            get {
                return (Value & 0b00010000) >> 4;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 4 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit4 {
            get => Bit4 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 5
        /// </summary>
        public int Bit5 {
            get {
                return (Value & 0b00100000) >> 5;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 5 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit5 {
            get => Bit5 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 6
        /// </summary>
        public int Bit6 {
            get {
                return (Value & 0b01000000) >> 6;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 6 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit6 {
            get => Bit6 == 1;
        }

        /// <summary>
        /// Возвращает значение бита в позиции 7
        /// </summary>
        public int Bit7 {
            get {
                return (Value & 0b10000000) >> 7;
            }
        }

        /// <summary>
        /// Возвращает true, если бит в позиции 7 установлен, иначе возвращает false
        /// </summary>
        public bool IsBit7 {
            get => Bit7 == 1;
        }


        /// <summary>
        /// Конструктор без параметров. Создаст экземпляр AinByteValue, где биты во всех позициях установлены в 0
        /// </summary>
        public AinByteValue() {
            Value = 0;
        }

        /// <summary>
        /// Конструктор с параметрами. Создаст экезмпляр AinByteValue по заданному параметру <paramref name="value"/>
        /// </summary>
        /// <param name="value">значение параметра определит внутреннее значение для данного экземпляра AinByteValue</param>
        public AinByteValue(byte value) {
            Value = value;
        }

        /// <summary>
        /// Сбрасывает все биты во всех позициях (от 0 до 7) в значение 0
        /// </summary>
        public void ResetAllBits() {
            Value = 0;
        }

        /// <summary>
        /// Устанавливает все биты во всех позициях (от 0 до 7) в значение 1
        /// </summary>
        public void SetAllBits() {
            Value = 255;
        }

        /// <summary>
        /// Создаёт экземпляр AinByteValue из заданного значения <paramref name="value"/>
        /// </summary>
        /// <param name="value">значение, которым будет проинициализирован новый экземпляр AinByteValue при его создании</param>
        /// <returns></returns>
        public static AinByteValue From(byte value) {
            return new AinByteValue(value);
        }

        /// <summary>
        /// Устанавливает или сбрасывает биты в позициях, задаваемых параметром
        /// <paramref name="bitsIndices"/>.
        /// Если массив индексов битов содержит значения, находящиеся в корректных пределах
        /// (от 0 до 7), то функция установит биты в заданных позициях и вернёт true.
        /// В случае, если <paramref name="bitsIndices"/> содержит хотя бы один индекс бита,
        /// выходящий за допустимые пределы (от 0 до 7), функция вернёт false, однако установит
        /// требуемое значение для битов, значения индексов для которых были корректно переданы  
        /// в параметре <paramref name="bitsIndices"/> и находятся в допустимых пределах.
        /// </summary>
        /// <param name="isSet">значение для установки битов в заданных позициях: true - установить бит, false - сбросить бит</param>
        /// <param name="bitsIndices">массив, содержащий индексы тех битов, для которых требуется установить заданное значение</param>
        /// <returns></returns>
        public bool SetBits(bool isSet, params byte[] bitsIndices) {
            if (bitsIndices == null) {
                return false;
            }
            bool isError = false;
            foreach (byte bitIndex in bitsIndices) {
                switch (bitIndex) {
                    case 0:
                        SetBit0(isSet);
                        break;
                    case 1:
                        SetBit1(isSet);
                        break;
                    case 2:
                        SetBit2(isSet);
                        break;
                    case 3:
                        SetBit3(isSet);
                        break;
                    case 4:
                        SetBit4(isSet);
                        break;
                    case 5:
                        SetBit5(isSet);
                        break;
                    case 6:
                        SetBit6(isSet);
                        break;
                    case 7:
                        SetBit7(isSet);
                        break;
                    default:
                        isError = true;
                        break;
                }
            }
            return !isError;
        }

        /// <summary>
        /// Закрытый метод класса. Устанавливает заданный бит во внутреннем значении экземпляра, на основании
        /// переденных параметров.
        /// </summary>
        /// <param name="isSet">true - если значение бита нужно установить, false - если значение бита нужно сбросить</param>
        /// <param name="bitwiseAndValue">значение, определяющее бит и его позицию для установки и сброса; используется при операции побитового 'И' ('bitwise and') по отношению к внутреннему значению экземпляра AinByteValue</param>
        /// <param name="increment">определяет значение, которое необходимо прибавить к внутреннему значению экземпляра AinByteValue после осуществления операции побитового 'И' ('bitwise and')</param>
        private void SetBitInternal(bool isSet, int bitwiseAndValue, byte increment) {
            if (isSet) {
                Value = (byte)((Value & bitwiseAndValue) + increment);
            } else {
                Value = (byte)(Value & bitwiseAndValue);
            }
        }

        /// <summary>
        /// Устанавливает бит в позиции 0 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 0: true - установить бит, false - сбросить бит</param>
        public void SetBit0(bool isSet) {
            SetBitInternal(isSet, 0b1111_1110, 1);
        }

        /// <summary>
        /// Устанавливает бит в позиции 1 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 1: true - установить бит, false - сбросить бит</param>
        public void SetBit1(bool isSet) {
            SetBitInternal(isSet, 0b1111_1101, 2);
        }

        /// <summary>
        /// Устанавливает бит в позиции 2 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 2: true - установить бит, false - сбросить бит</param>
        public void SetBit2(bool isSet) {
            SetBitInternal(isSet, 0b1111_1011, 4);
        }

        /// <summary>
        /// Устанавливает бит в позиции 3 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 3: true - установить бит, false - сбросить бит</param>
        public void SetBit3(bool isSet) {
            SetBitInternal(isSet, 0b1111_0111, 8);
        }

        /// <summary>
        /// Устанавливает бит в позиции 4 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 4: true - установить бит, false - сбросить бит</param>
        public void SetBit4(bool isSet) {
            SetBitInternal(isSet, 0b1110_1111, 16);
        }

        /// <summary>
        /// Устанавливает бит в позиции 5 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 5: true - установить бит, false - сбросить бит</param>
        public void SetBit5(bool isSet) {
            SetBitInternal(isSet, 0b1101_1111, 32);
        }

        /// <summary>
        /// Устанавливает бит в позиции 6 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 6: true - установить бит, false - сбросить бит</param>
        public void SetBit6(bool isSet) {
            SetBitInternal(isSet, 0b1011_1111, 64);
        }

        /// <summary>
        /// Устанавливает бит в позиции 7 в заданное значение
        /// </summary>
        /// <param name="isSet">значение для установки бита в позиции 7: true - установить бит, false - сбросить бит</param>
        public void SetBit7(bool isSet) {
            SetBitInternal(isSet, 0b0111_1111, 128);
        }
    }

 

В качестве заключения хочу сказать, что аналогов готовых классов для C#, которые бы могли управлять отдельными битами внутри байта мне найти не удалось на просторах интернета (отчасти это и привело к подобному эксперименту - создать класс, "управляющий битами байта").

Относительно использования памяти в программах, где может использоваться множество bool-флажков и мог бы пригодиться рассмотренный нами класс AinByteValue: детальный анализ использования памяти во время исполнения программы, равно как и доказательства какой-либо существенной экономии памяти при работе с этим классом по сравнению с использованием множества полей с типом bool, я пока не проводил. Но по моим ощущениям, определённая экономия по памяти должна присутствовать, хотя для этого целевая программа действительно должна быть довольно специфичной и оперировать большим (или огромным) количеством флагов типа bool или создавать большое/огромное количество объектов в рантайме. Возможно, это может кому-то встретиться при разработке больших программ или в мире гейм-дева. Мне было бы интересно провести дополнительное исследование по вопросам экономии памяти рассмотренным классом, и, по возможности, я постараюсь его провести, ну а пока вопрос для меня остаётся открытым, и я был бы крайне рад, если бы опытные разработчики C#, знакомые со всеми тонкостями управления памятью, поделились в комментариях к статье своим мнением в части полезности (или бесполезности 🙃) рассмотренного в статье класса и экономии им памяти в указанных случаях.

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

Пока на этом всё, спасибо за внимание и успехов!

 

 

Яндекс.Метрика