Вступ
Числа, які ми використовуємо у повсякденному житті, належать десятковій (decimal) системі числення. Окрім десяткової системи, нам також відомі двійкова (binary), вісімкова (octal) та шістнадцяткова (hexadecimal) системи числення. У цій статті ми розглянемо двійкову систему числення, яка є основою комп'ютерних систем, і операції з бітами за допомогою C#.
Двійкова (binary) система числення
Як вже згадувалося, система числення, яку ми використовуємо в повсякденному житті, є десятковою. У цій системі числа складаються з цифр 0, 1, 2, 3, … і так далі. В двійковій системі використовуються лише цифри 0 і 1. Кожна цифра в двійковій системі називається бітом.
Двійкова система числення та її перетворення в десяткову. Малюнок 1.
Як показано на малюнку 1, значення цифр у двійковій системі вказується за допомогою степенів числа 2, що відповідають місцю кожної цифри, при цьому рахунок йде з правого боку (де n = місце цифри - 1). Для того, щоб перетворити двійкове число в десяткове, необхідно додати значення, яке відповідає кожному одиничному біту, залежно від його позиції.
Читання двійкових чисел
Тепер давайте подивимося, як у мові програмування C# працювати з двійковими числами.
Спочатку зазначу, що код, який ми пишемо в IDE, називається source code (джерельний код), і операційні системи безпосередньо його не розуміють. Для цього необхідно виконати процес компіляції. Цей процес може відрізнятися в залежності від мови програмування.
В C# джерельний код через compiler спочатку перетворюється на intermediate code (проміжний код або managed code). Потім цей проміжний код за допомогою JIT compiler (Just-In-Time compiler) перетворюється на machine code (машинний код), який уже є послідовністю нулів і одиниць, яку комп'ютер може зрозуміти.
В C# немає окремого data type для вираження двійкових чисел. У прикладах сьогоднішньої статті ми використовуватимемо стандартний тип даних int
.
Але якщо ми хочемо записати число в двійковій системі, перед числом потрібно використовувати префікс 0b
. Це вказує compiler-у, що це число в двійковій системі і його потрібно перетворити в десяткову.
Важливо зазначити, що для типу int
в C# відведено 32 біти. Тобто, наші двійкові числа можуть займати максимум 32 розряди (цифри в двійковій системі називаються біти).
Методи розширення ConvertToBinary()
Оскільки компілятор при спробі читати число, записане в двійковій системі, автоматично виведе його в десятковому вигляді, давайте створимо допоміжний метод розширення для перетворення числа.
public static class Extensions {
// Параметр placeCount вказує, скільки розрядів з максимальних 32 ми хочемо побачити (за замовчуванням 8)
public static string ConvertToBinary(this int value, int placeCount = 8) {
/*
Для перетворення числа типу int в двійкову систему ми використовуємо
метод Convert.ToString (int value, int toBase).
toBase параметр вказує, яку систему числення ми хочемо використовувати (2, 8, 10 або 16)
*/
return Convert.ToString(value, 2).PadLeft(placeCount, '0');
}
}
Створивши метод розширення, давайте подивимося, як ми можемо визначати і читати числа.
// Приклад 1: Число 40 в десятковій і двійковій системах
int x = 40; // десяткова система 40
int y = 0b_101000; // двійкова система 40
Console.WriteLine($"Value: {x} - Binary: {x.ConvertToBinary()}"); // Value: 40 - Binary: 00101000
Console.WriteLine($"Value: {y} - Binary: {y.ConvertToBinary()}"); // Value: 40 - Binary: 00101000
// Приклад 2: Число 100 в десятковій і двійковій системах
int a = 100; // десяткова система 100
int b = 0b_1100100; // двійкова система 100
Console.WriteLine($"Value: {a} - Binary: {a.ConvertToBinary()}"); // Value: 100 - Binary: 01100100
Console.WriteLine($"Value: {b} - Binary: {b.ConvertToBinary()}"); // Value: 100 - Binary: 01100100
// Приклад 3: Число 255 в десятковій і двійковій системах
int c = 255; // десяткова система 255
int d = 0b_11111111; // двійкова система 255
Console.WriteLine($"Value: {c} - Binary: {c.ConvertToBinary()}"); // Value: 255 - Binary: 11111111
Console.WriteLine($"Value: {d} - Binary: {d.ConvertToBinary()}"); // Value: 255 - Binary: 11111111
// Приклад 4: Число 2020 в десятковій і двійковій системах
int e = 2020; // десяткова система 2020
int f = 0b_0000011111100100; // двійкова система 2020
Console.WriteLine($"Value: {e} - Binary: {e.ConvertToBinary(16)}"); // Value: 2020 - Binary: 0000011111100100
Console.WriteLine($"Value: {f} - Binary: {f.ConvertToBinary(16)}"); // Value: 2020 - Binary: 0000011111100100
Як видно з прикладів, навіть якщо ми використовуємо префікс 0b
для запису числа в двійковій системі, при читанні коду воно буде відображатися в десятковому вигляді. В перших трьох прикладах числа вміщуються в 8-розрядний простір, а в останньому прикладі, оскільки число велике, ми заповнюємо порожні місця нулями до 16 розрядів (максимум 32 розряди).
Тепер, коли ми навчилися працювати з числами в двійковій системі в C#, давайте перейдемо до інших операторів.
Операції з двійковими числами. Побітові оператори (Bitwise operators)
В C# є відомі логічні оператори: AND (&&), OR (||) та NOT (!). Побітові оператори (Bitwise operators) схожі, але мають деякі відмінності. Ось вони:
- AND (&): Порівнює кожен біт двох чисел, і результатом буде 1 лише тоді, коли обидва біти рівні 1.
- OR (|): Порівнює кожен біт двох чисел, і результатом буде 1, якщо хоча б один з бітів рівний 1.
- XOR (^): Оператор виключаючого OR, результат буде 1, якщо біти різні, і 0, якщо однакові.
- LEFT SHIFT (<<): Зсуває число вліво на задану кількість розрядів, заповнюючи праві біти нулями.
- RIGHT SHIFT (>>): Зсуває число вправо на задану кількість розрядів, заповнюючи ліві біти нулями. Видаляє біти з правого боку.
- NOT (~): Інвертує всі біти числа, тобто змінює 0 на 1, а 1 на 0.
Розглянемо ці оператори на прикладах:
Приклад 1: Оператор AND (&)
Оператор AND порівнює кожен біт двох чисел і повертає 1 лише тоді, коли обидва біти рівні 1. В усіх інших випадках результат буде 0.
int a = 5; // Binary: 0101
int b = 3; // Binary: 0011
int result = a & b; // Binary: 0001, Decimal: 1
// 5 & 3 = 1 (Value: 5 & 3 = 1 /// Binary: 00000101 & 00000011 = 00000001)
Console.WriteLine($"{a} & {b} = {result} (Value: {a} & {b} = {result} /// Binary: {a.ConvertToBinary()} & {b.ConvertToBinary()} = {result.ConvertToBinary()})");
Приклад 2: Оператор OR (|)
Оператор OR порівнює кожен біт двох чисел і повертає 1, якщо хоча б один з бітів рівний 1.
Коли обидва біти 0, результат буде 0.
int a = 5; // Binary: 0101
int b = 3; // Binary: 0011
int result = a | b; // Binary: 0111, Decimal: 7
// 5 | 3 = 7 (Value: 5 | 3 = 7 /// Binary: 00000101 | 00000011 = 00000111)
Console.WriteLine($"{a} | {b} = {result} (Value: {a} | {b} = {result} /// Binary: {a.ConvertToBinary()} | {b.ConvertToBinary()} = {result.ConvertToBinary()})");
Приклад 3: Оператор XOR (^)
Оператор XOR повертає 1 лише тоді, коли біти різні, і 0 — коли вони однакові.
int a = 5; // Binary: 0101
int b = 3; // Binary: 0011
int result = a ^ b; // Binary: 0110, Decimal: 6
// 5 ^ 3 = 6 (Value: 5 ^ 3 = 6 /// Binary: 00000101 ^ 00000011 = 00000110)
Console.WriteLine($"{a} ^ {b} = {result} (Value: {a} ^ {b} = {result} /// Binary: {a.ConvertToBinary()} ^ {b.ConvertToBinary()} = {result.ConvertToBinary()})");
Приклад 4: Оператор LEFT SHIFT (<<)
Оператор LEFT SHIFT зсуває число вліво на задану кількість розрядів.
int a = 5; // Binary: 0101
int result = a << 2; // Binary: 10100, Decimal: 20
// 5 << 2 = 20 (Value: 5 << 2 = 20 /// Binary: 00000101 << 2 = 00010100)
Console.WriteLine($"{a} << 2 = {result} (Value: {a} << 2 = {result} /// Binary: {a.ConvertToBinary()} << 2 = {result.ConvertToBinary()})");
Приклад 5: Оператор RIGHT SHIFT (>>)
Оператор RIGHT SHIFT зсуває число вправо на задану кількість розрядів.
int a = 5; // Binary: 0101
int result = a >> 2; // Binary: 0001, Decimal: 1
// 5 >> 2 = 1 (Value: 5 >> 2 = 1 /// Binary: 00000101 >> 2 = 00000001)
Console.WriteLine($"{a} >> 2 = {result} (Value: {a} >> 2 = {result} /// Binary: {a.ConvertToBinary()} >> 2 = {result.ConvertToBinary()})");
Приклад 6: Оператор NOT (~)
Оператор FLIP інвертує число, змінюючи 0 на 1, а 1 на 0.
int a = 5; // Binary: 0101
int result = ~a; // Binary: 1010, Decimal: -6
// ~5 = -6 (Value: ~5 = -6 /// Binary: 00000101 = 11111111111111111111111111111010)
Console.WriteLine($"~{a} = {result} (Value: ~{a} = {result} /// Binary: {a.ConvertToBinary()} = {result.ConvertToBinary()})");
Ці оператори широко використовуються для виконання різноманітних обчислень і маніпуляцій з двійковими числами, оптимізації продуктивності.
Що таке Bitmask і Flag Enums? Як їх використовувати?
Що таке Bitmasking?
Bitmasking — це операції, які виконуються над двійковими (binary) представленнями чисел в комп'ютерних науках. Вони здійснюються за допомогою побітових операторів, які ми вивчили вище.
Переваги Bitmasking:
- Висока швидкість: Операції на рівні бітів зазвичай виконуються швидше за традиційні операції. Bitmasking дозволяє ефективно і швидко виконувати складні операції над великими даними.
- Ефективність пам'яті: Bitmasking дозволяє зберігати великі дані компактно та ефективно з погляду пам'яті.
- Універсальність: Bitmasking є універсальним методом, який може бути застосований у різних областях, таких як мережеві протоколи, криптографія та стиснення даних.
- Маскування та фільтрація даних: Bitmasking може використовуватися для маскування та фільтрації даних, виконуючи операції on або off на окремих бітах.
Недоліки Bitmasking:
- Обмежений діапазон: Bitmasking має обмежений діапазон, оскільки він може представляти лише певну кількість значень. Якщо потрібно представити більше значень, це неможливо.
- Складність коду: Bitmasking може ускладнити програму, особливо коли використовуються складні побітові операції.
- Налагодження: Побітові операції можуть викликати помилки, які важко знайти. Наприклад, помилка у розташуванні побітового оператора може зайняти багато часу для виявлення.
- Складність у читанні коду: Використання bitmasking може зробити код коротшим, але для людей, які не знайомі з цією технікою, розуміння коду може бути важким.
В результаті, з часом оновлювати його може бути складніше.
Flag Enums
Під час роботи над проєктами можуть виникати ситуації, коли потрібно зберігати кілька значень в одному змінному типі enum. Наприклад, для прав користувача необхідно задати кілька значень enum (Read, Write), в такому випадку потрібно використовувати масив або список для цього змінного. Але з точки зору оптимізації продуктивності, це не є ідеальним варіантом. Замість цього є зручніша альтернатива: Flag Enums.
Як вам відомо, в enum є значення, що відповідають іменам. Хоча імена є рядковими, значення мають числовий формат. І як ми вже вивчили, над числовими даними можна виконувати побітові операції.
Приклад Flag Enum
При використанні Flag Enums слід застосовувати атрибут Flags. Цей атрибут вказує, що значення enum будуть оброблятися за допомогою побітових операцій. Крім того, значення enum повинні бути задані у форматі 2^n. Значення можна визначати як десяткові числа або за допомогою оператора зсуву (<<). Однак на практиці краще використовувати один з цих методів.
// Flag Enum-у слід давати назви в множині, звичайні enum - в однині
[Flags]
public enum Permissions
{
None = 0, // 0000 - 0
Read = 1, // 0001 - 1
Write = 1 << 1, // 0010 - 2
Execute = 4, // 0100 - 4
Delete = 1 << 3 // 1000 - 8
// як видно, значення можна записувати як у десятковій системі, так і за допомогою оператора зсуву
}
У цьому прикладі enum Permissions
визначає п'ять різних значень: None, Read, Write, Execute та Delete. Ці значення базуються на ступенях числа 2.
Давайте розглянемо операції над Flag Enums:
1. Як додавати значення
Значення enum можна об'єднати за допомогою побітового оператора OR (|
). При цьому додається нове значення.
// 1. Як додавати значення
Permissions userPermissions = Permissions.Read | Permissions.Execute; // 0001 | 0100 = 0101;
2. Як читати значення
Атрибут Flags вказує, що значення в enum — це біти. Тому, читаючи значення, ми отримуємо їх не в десятковому форматі, а у вигляді строкових значень, що зручніше для сприйняття.
// 2. Як читати значення
Console.WriteLine($"Права користувача: {userPermissions}"); // Права користувача: Read, Execute
Зазначу, що якщо ми видалимо атрибут Flags з enum, то в консолі результат буде виглядати так: “Права користувача: 5” (десяткове значення 0101).
3. Як перевірити, чи встановлено значення
Для перевірки наявності значення використовують побітовий оператор AND (&), щоб знайти перетин. Якщо перетин є, значить, значення додано. Як альтернатива, можна використовувати метод .HasFlag().
// 3. Як перевірити, чи встановлено значення
// Перевірка за допомогою оператора AND (&)
if((userPermissions & Permissions.Read) == Permissions.Read) {
Console.WriteLine("Є дозвіл на читання"); // Є дозвіл на читання
} else Console.WriteLine("Немає дозволу на читання");
// або
// Перевірка за допомогою .HasFlag()
if(userPermissions.HasFlag(Permissions.Write)) {
Console.WriteLine("Є дозвіл на запис");
} else Console.WriteLine("Немає дозволу на запис"); // Немає дозволу на запис
4. Як видалити значення
Значення можна видаляти за допомогою побітового оператора NOT (~), знаходячи відмінні біти і присвоюючи отримане значення змінним правам. Альтернативний метод — використання оператора XOR(^), який працює з різними біти.
// 4. Як видалити значення
// Видалення за допомогою операторів AND та NOT
// userPermissions = userPermissions & ~Permissions.Read;
userPermissions &= ~Permissions.Read;
Console.WriteLine($"Права користувача: {userPermissions}"); // Права користувача: Execute
// або
// Видалення за допомогою оператора XOR
// userPermissions = userPermissions ^ Permissions.Execute;
userPermissions ^= Permissions.Execute;
Console.WriteLine($"Права користувача: {userPermissions}"); // Права користувача: None
Висновок
Робота з двійковими числами та побітовими операціями дуже корисна в світі програмування.
Двійкова система числення (binary) допомагає зрозуміти, як комп'ютери представляють і обробляють дані. Загалом, біти використовуються для маніпулювання складними даними, забезпечуючи оптимізацію швидкості та пам'яті під час операцій. Операції над ними забезпечують як високу продуктивність, так і ефективне використання пам'яті, але їх застосування та зберігання в деяких випадках може бути складним.
Перекладено з: C#-da binary ədədlərlə işləmək. Bitwise operatorları, Bitmask və Flag Enums