- Для чого ви використовували побітові операції?
- чому вони такі зручні?
- хтось може порекомендувати ДУЖЕ простий підручник?
Відповіді:
Незважаючи на те, що всі схоплені на прапори використання, це не єдина програма побітових операторів (хоча, мабуть, найпоширеніша). Також C # - мова досить високого рівня, що інші методи, ймовірно, будуть застосовуватися рідко, але все одно варто їх знати. Ось що я можу думати:
Оператори <<
and >>
можуть швидко помножити на потужність 2. Звичайно, оптимізатор .NET JIT, мабуть, зробить це за вас (і будь-який пристойний компілятор іншої мови), але якщо ви справді хвилюєтесь кожну мікросекунду, ви просто можу написати це, щоб бути впевненим.
Ще одним загальним використанням цих операторів є набивання двох 16-бітових цілих чисел в одне 32-бітове ціле число. Подібно до:
int Result = (shortIntA << 16 ) | shortIntB;
Це характерно для прямого взаємодії з функціями Win32, які іноді використовують цей трюк із застарілих причин.
І, звичайно, ці оператори корисні, коли ви хочете заплутати недосвідчених, як, наприклад, при наданні відповіді на питання домашнього завдання. :)
У будь-якому реальному коді вам буде набагато краще, використовуючи натомість множення, оскільки він має набагато кращу читабельність, і JIT в будь-якому випадку оптимізує його shl
та shr
інструкції, тому не вимагає покарання за продуктивність.
Досить багато цікавих прийомів стосуються ^
оператора (XOR). Це насправді дуже потужний оператор через наступні властивості:
A^B == B^A
A^B^A == B
A^B
тоді неможливо сказати, що A
і що B
є, але якщо ви знаєте одне з них, можете обчислити інше.Кілька хитрощів, які я бачив за допомогою цього оператора:
Заміна двох цілочисельних змінних без посередницької змінної:
A = A^B // A is now XOR of A and B
B = A^B // B is now the original A
A = A^B // A is now the original B
Подвійно пов’язаний список із лише однією додатковою змінною на елемент. Це мало буде використовувати в C #, але це може стати в нагоді для низькорівневого програмування вбудованих систем, де кожен байт має значення.
Ідея полягає в тому, що ви стежите за вказівником на перший елемент; вказівник на останній елемент; і для кожного предмета, який ви відстежуєте pointer_to_previous ^ pointer_to_next
. Таким чином ви можете пройти по списку з будь-якого кінця, але загальні накладні витрати складають лише половину від традиційного пов'язаного списку. Ось код C ++ для обходу:
ItemStruct *CurrentItem = FirstItem, *PreviousItem=NULL;
while ( CurrentItem != NULL )
{
// Work with CurrentItem->Data
ItemStruct *NextItem = CurrentItem->XorPointers ^ PreviousItem;
PreviousItem = CurrentItem;
CurrentItem = NextItem;
}
Для переходу з кінця потрібно просто змінити перший рядок з FirstItem
на LastItem
. Це ще одна економія пам’яті.
Ще одне місце, де я ^
регулярно використовую оператор в C # - це коли мені потрібно обчислити HashCode для мого типу, який є складеним типом. Подібно до:
class Person
{
string FirstName;
string LastName;
int Age;
public int override GetHashCode()
{
return (FirstName == null ? 0 : FirstName.GetHashCode()) ^
(LastName == null ? 0 : LastName.GetHashCode()) ^
Age.GetHashCode();
}
}
Я використовую побітові оператори для безпеки в своїх додатках. Я зберігатиму різні рівні всередині Enum:
[Flags]
public enum SecurityLevel
{
User = 1, // 0001
SuperUser = 2, // 0010
QuestionAdmin = 4, // 0100
AnswerAdmin = 8 // 1000
}
А потім призначте користувачеві їх рівні:
// Set User Permissions to 1010
//
// 0010
// | 1000
// ----
// 1010
User.Permissions = SecurityLevel.SuperUser | SecurityLevel.AnswerAdmin;
А потім перевірте дозволи у виконуваній дії:
// Check if the user has the required permission group
//
// 1010
// & 1000
// ----
// 1000
if( (User.Permissions & SecurityLevel.AnswerAdmin) == SecurityLevel.AnswerAdmin )
{
// Allowed
}
Я не знаю, наскільки практичним є вирішення судоку, яке ви вважаєте, але це припустимо.
Уявіть, що ви хочете написати вирішувач судоку або навіть просто просту програму, яка показує вам дошку та дозволяє вам самостійно розгадати головоломку, але гарантує, що ходи є законними.
Сама дошка, швидше за все, буде представлена двовимірним масивом, як:
uint [, ] theBoard = new uint[9, 9];
Значення 0
означає, що комірка все ще порожня, а значення з діапазону [1u, 9u] - це фактичні значення на платі.
А тепер уявіть, що ви хочете перевірити, чи є якийсь крок законним. Очевидно, що ви можете зробити це за допомогою декількох циклів, але бітові маски дозволяють зробити це набагато швидше. У простій програмі, яка просто забезпечує дотримання правил, це не має значення, але в засобі вирішення проблеми це може.
Ви можете підтримувати масиви бітових масок, які зберігають інформацію про числа, які вже вставлені в кожен рядок, кожен стовпець a та кожне поле 3x3.
uint [] maskForNumbersSetInRow = new uint[9];
uint [] maskForNumbersSetInCol = new uint[9];
uint [, ] maskForNumbersSetInBox = new uint[3, 3];
Зіставлення числа з бітовим шаблоном, причому один біт відповідає цьому набору чисел, дуже простий
1 -> 00000000 00000000 00000000 00000001
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000100
...
9 -> 00000000 00000000 00000001 00000000
У C # ви можете обчислити бітовий шаблон таким чином ( value
є an uint
):
uint bitpattern = 1u << (int)(value - 1u);
У рядку вище, що 1u
відповідає бітовому шаблону 00000000 00000000 00000000 00000001
, зсувається ліворуч на value - 1
. Якщо, наприклад value == 5
, ви отримаєте
00000000 00000000 00000000 00010000
На початку маска для кожного рядка, стовпця та поля є 0
. Щоразу, коли ви ставите якесь число на дошці, ви оновлюєте маску, тому встановлюється біт, що відповідає новому значенню.
Припустимо, ви вставляєте значення 5 у рядок 3 (рядки та стовпці нумеруються від 0). Маска для ряду 3 зберігається в maskForNumbersSetInRow[3]
. Припустимо також, що до вставки вже були числа {1, 2, 4, 7, 9}
в рядку 3. Бітовий шаблон у масці maskForNumbersSetInRow[3]
виглядає так:
00000000 00000000 00000001 01001011
bits above correspond to:9 7 4 21
Мета - встановити біт, що відповідає значенню 5 у цій масці. Ви можете зробити це за допомогою побітового або оператора ( |
). Спочатку ви створюєте бітовий шаблон, що відповідає значенню 5
uint bitpattern = 1u << 4; // 1u << (int)(value - 1u)
а потім ви використовуєте operator |
для встановлення біта в масціmaskForNumbersSetInRow[3]
maskForNumbersSetInRow[3] = maskForNumbersSetInRow[3] | bitpattern;
або за допомогою коротшої форми
maskForNumbersSetInRow[3] |= bitpattern;
00000000 00000000 00000001 01001011
|
00000000 00000000 00000000 00010000
=
00000000 00000000 00000001 01011011
Тепер ваша маска вказує на наявність значень {1, 2, 4, 5, 7, 9}
у цьому рядку (рядок 3).
Якщо ви хочете перевірити, якщо в рядку є якесь значення, ви можете використовувати, operator &
щоб перевірити, чи встановлений відповідний біт у масці. Якщо результат цього оператора, застосованого до маски та бітового шаблону, що відповідає цьому значенню, є ненульовим, значення вже є в рядку. Якщо результат 0, значення не в рядку.
Наприклад, якщо ви хочете перевірити, чи є значення 3 у рядку, ви можете зробити це таким чином:
uint bitpattern = 1u << 2; // 1u << (int)(value - 1u)
bool value3IsInRow = ((maskForNumbersSetInRow[3] & bitpattern) != 0);
00000000 00000000 00000001 01001011 // the mask
|
00000000 00000000 00000000 00000100 // bitpattern for the value 3
=
00000000 00000000 00000000 00000000 // the result is 0. value 3 is not in the row.
Нижче наведено методи встановлення нового значення на дошці, підтримання відповідних бітових масок та перевірки законності переміщення.
public void insertNewValue(int row, int col, uint value)
{
if(!isMoveLegal(row, col, value))
throw ...
theBoard[row, col] = value;
uint bitpattern = 1u << (int)(value - 1u);
maskForNumbersSetInRow[row] |= bitpattern;
maskForNumbersSetInCol[col] |= bitpattern;
int boxRowNumber = row / 3;
int boxColNumber = col / 3;
maskForNumbersSetInBox[boxRowNumber, boxColNumber] |= bitpattern;
}
Маючи маски, ви можете перевірити, чи переїзд є законним, наприклад:
public bool isMoveLegal(int row, int col, uint value)
{
uint bitpattern = 1u << (int)(value - 1u);
int boxRowNumber = row / 3;
int boxColNumber = col / 3;
uint combinedMask = maskForNumbersSetInRow[row] | maskForNumbersSetInCol[col]
| maskForNumbersSetInBox[boxRowNumber, boxColNumber];
return ((theBoard[row, col] == 0) && ((combinedMask & bitpattern) == 0u);
}
Десятки прикладів дрібного покручування тут
Код на C, але ви можете легко адаптувати його до C #
Їх можна використовувати для цілого навантаження різних додатків, ось питання, які я вже розміщував тут, і використовує побітові операції:
Побітове І, побітове Включене АБО питання в Java
Для інших прикладів подивіться (скажімо) позначені переліки.
У моєму прикладі я використовував побітові операції, щоб змінити діапазон двійкового числа з -128 ... 127 на 0..255 (змінивши його подання з підписаного на беззнаковий).
стаття MSN тут ->
http://msdn.microsoft.com/en-us/library/6a71f45d%28VS.71%29.aspx
корисно.
І, хоча це посилання:
дуже технічний, він охоплює все.
HTH
Я повинен був би сказати, що одним із найпоширеніших застосувань є модифікація бітових полів для стиснення даних. Ви в основному бачите це в програмах, які намагаються бути економічними з пакетами.
Однією з найпоширеніших речей, для якої я їх використовую в C #, є створення хеш-кодів. Існує кілька досить хороших методів хешування, які їх використовують. Наприклад, для координованого класу з X та Y, які обидва я міг використовувати:
public override int GetHashCode()
{
return x ^ ((y << 16) | y >> 16);
}
Це швидко генерує число, яке гарантовано буде рівним при отриманні рівним об'єктом (припускаючи, що рівність означає, що параметри X і Y однакові в обох об'єктах, що порівнюються), одночасно не створюючи шаблонів зіткнення для малозначущих об'єктів (ймовірно найпоширеніші у більшості додатків).
Інший - поєднання переліків прапорів. НапрRegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase
Є деякі низькорівневі операції, які частіше не потрібні, коли ви кодуєте на такому фреймворку, як .NET (наприклад, в C # мені не потрібно буде писати код для перетворення UTF-8 в UTF-16, це для мене в рамки), але, звичайно, хтось повинен був написати цей код.
Існує декілька прийомів бітового обертання, наприклад округлення до найближчого двійкового числа (наприклад, округлення до 1010 до 10000):
unchecked
{
--x;
x |= (x >> 1);
x |= (x >> 2);
x |= (x >> 4);
x |= (x >> 8);
x |= (x >> 16);
return ++x;
}
Які корисні, коли вони вам потрібні, але це, як правило, не дуже часто.
Нарешті, ви також можете використовувати їх для мікрооптимізації математики, наприклад << 1
замість цього, * 2
але я включаю це лише для того, щоб сказати, що це, як правило, погана ідея, оскільки вона приховує намір реального коду, майже нічого не економить у продуктивності та може приховати деякі незначні помилки .
Ви будете використовувати їх з різних причин:
Я впевнений, що ти можеш думати про інших.
З огляду на це, іноді потрібно запитати себе: чи варто пам’ять та підвищення продуктивності докласти зусиль? Написавши такий код, дайте йому трохи відпочити і поверніться до нього. Якщо ви боретеся з цим, перепишіть з більш ремонтопридатним кодом.
З іншого боку, іноді цілком логічно використовувати побітові операції (подумайте про криптографію).
А ще краще, нехай його прочитає хтось інший і широко документує.
Ігри!
Ще в ті часи я використовував його для представлення фігур гравця "Реверсі". Це 8X8, тому мені знадобився long
тип, і тоді, наприклад, якщо ви хочете знати, де всі фігури на борту - ви or
обоє гравців.
Якщо ви хочете всі можливі кроки гравця, скажіть праворуч - ви >>
представляєте фігури гравця по одному, а AND
разом із фігурами супротивника перевіряйте, чи є зараз загальні одиниці 1 (це означає, що справа від вас є фігура суперника). Тоді ви продовжуєте це робити. якщо ти повернешся до своїх частин - ніякого руху. Якщо ви дістанетеся до чіткого біта - ви можете переїхати туди і захопити всі шматки в дорозі.
(Цей прийом широко використовується у багатьох видах настільних ігор, включаючи шахи)