プログラミングを勉強していると、
AND
OR
XOR
NOT
というビット演算が出てきます。
計算方法だけ見ると、
1 AND 1 = 1
1 OR 0 = 1
のような話で終わってしまい、
「これを実際に何に使うの?」
と感じることがあります。
ビット演算が分かりやすくなる例の一つが、
複数のON/OFF状態を1つの整数で管理する
方法です。
この記事では「ユーザー権限」を例に、AND・OR・XOR・NOTが実際のコードでどう使えるのかを整理します。
4つの権限を考える
あるシステムに、次の4つの権限があるとします。
READ 読み取り
WRITE 書き込み
DELETE 削除
ADMIN 管理者
普通に管理するなら、
const permissions = {
read: true,
write: true,
delete: false,
admin: false
};
のようにできます。
もちろんこれでも問題ありません。
ただし、それぞれの状態を1bitずつ割り当てると、1つの整数として管理することもできます。
1bitずつ権限を割り当てる
たとえば次のようにします。
READ = 0001
WRITE = 0010
DELETE = 0100
ADMIN = 1000
10進数では、
READ = 1
WRITE = 2
DELETE = 4
ADMIN = 8
です。
JavaScriptなら、
const READ = 1 << 0; // 0001
const WRITE = 1 << 1; // 0010
const DELETE = 1 << 2; // 0100
const ADMIN = 1 << 3; // 1000
と書けます。
1 << nは、1を左へnビットシフトするという意味です。
ORで権限を追加する
READとWRITEの両方を持つユーザーを作りたいとします。
READ 0001
WRITE 0010
ORを取ると、
0001
0010
----
0011
になります。
JavaScriptでは、
let permission = READ | WRITE;
console.log(permission);
// 3
です。
2進数では、
0011
なので、
READ = ON
WRITE = ON
という状態を1つの整数3で表せます。
ORは「フラグを立てる」
既存のユーザーにDELETE権限を追加したい場合もORを使えます。
現在、
0011
だったとします。
DELETEは、
0100
です。
permission = permission | DELETE;
結果は、
0011
0100
----
0111
になります。
つまり、
READ
WRITE
DELETE
の3つがONになりました。
省略して、
permission |= DELETE;
とも書けます。
ANDで権限を確認する
次は、
「このユーザーにWRITE権限があるか?」
を確認します。
現在の権限が、
0111
WRITEが、
0010
なら、ANDを取ります。
0111
0010
----
0010
結果が0ではないので、WRITEのbitが立っています。
JavaScriptなら、
if ((permission & WRITE) !== 0) {
console.log("書き込み可能");
}
です。
このパターンはビットマスクを使うコードで非常によく出てきます。
なぜANDで確認できるのか
ANDは、
両方が1のときだけ1
になります。
たとえば、
permission = 0111
WRITE = 0010
なら、WRITEの位置だけを取り出せます。
0111
0010
----
0010
これを「マスクする」と考えると分かりやすいです。
必要なbitだけを残して、ほかを0にしています。
このような値を、
ビットマスク
と呼びます。
権限がない場合
たとえばADMIN権限を確認します。
permission = 0111
ADMIN = 1000
ANDすると、
0111
1000
----
0000
です。
結果が0なのでADMIN権限はありません。
const hasAdmin = (permission & ADMIN) !== 0;
console.log(hasAdmin);
// false
となります。
権限を外すにはANDとNOTを使う
今度はWRITE権限を削除してみます。
現在、
permission = 0111
です。
WRITEは、
0010
なので、まずNOTで反転します。
4bitだけで考えると、
0010
↓ NOT
1101
です。
これを現在の権限とANDします。
0111
1101
----
0101
結果は、
READ = ON
WRITE = OFF
DELETE = ON
ADMIN = OFF
です。
JavaScriptでは、
permission &= ~WRITE;
と書けます。
ビットフラグではよく使う書き方です。
XORならON/OFFを反転できる
XORは、
同じなら0
違えば1
になる演算です。
これを使うと、特定のフラグだけON/OFFを切り替えられます。
たとえば、
permission ^= WRITE;
とすると、WRITEが、
ONならOFF
OFFならON
になります。
スイッチのような動作です。
XORを実際に見てみる
現在の権限が、
0101
だったとします。
WRITEは、
0010
です。
XORすると、
0101
0010
----
0111
WRITEがONになりました。
もう一度同じXORを実行すると、
0111
0010
----
0101
元に戻ります。
つまり、
permission ^= WRITE;
を実行するたびにWRITEが切り替わります。
まとめると4つの操作になる
ビットフラグを扱う基本操作は、かなりシンプルです。
フラグを追加
flags |= FLAG;
フラグがあるか確認
(flags & FLAG) !== 0
フラグを削除
flags &= ~FLAG;
フラグを反転
flags ^= FLAG;
この4つを理解すると、ビット演算の実用的な使い方がかなり見えてきます。
複数の権限を一度に確認する
READとWRITEの両方が必要な場合を考えます。
まず2つをORでまとめます。
const required = READ | WRITE;
2進数では、
0001
0010
----
0011
です。
ユーザーが両方持っているか確認するなら、
if ((permission & required) === required) {
console.log("READとWRITEの両方を持っています");
}
とできます。
ここで、
(permission & required) !== 0
としてしまうと、
「どちらか1つでも持っている」
場合にtrueになります。
両方必要なら、
(permission & required) === required
と比較するのがポイントです。
8bitなら8種類の状態を持てる
1bitで、
0
1
の2状態を表せます。
8bitなら、
00000000
から、
11111111
まであります。
それぞれのbitを独立したフラグとして使えば、
bit 0
bit 1
bit 2
bit 3
bit 4
bit 5
bit 6
bit 7
という8種類のON/OFF状態を1つの数値に格納できます。
16bitなら16種類、32bitなら32種類です。
なぜ値が1・2・4・8になるのか
フラグとして、
1
2
4
8
16
32
のような値を見かける理由もここにあります。
2進数では、
1 = 000001
2 = 000010
4 = 000100
8 = 001000
16 = 010000
32 = 100000
となり、それぞれ別のbitだけが1になっています。
つまり、
2の累乗
を使うことで、各フラグが重ならないようにできます。
3や5を単独フラグにしない理由
たとえば、
3 = 0011
です。
すでに2つのbitが立っています。
そのため、
const SOME_FLAG = 3;
のようにすると、独立した1つのフラグとして扱いにくくなります。
一方、
4 = 0100
なら1bitだけなので、ほかのフラグと安全に組み合わせられます。
だからビットフラグでは、
1, 2, 4, 8, 16...
がよく使われます。
16進数で書かれることも多い
ビットマスクは16進数で表現されることもあります。
たとえば、
11111111
は16進数なら、
FF
です。
00001111
なら、
0F
です。
長いビット列になるほど、16進数の方が読みやすくなります。
たとえば、
const MASK = 0xFF;
のようなコードを見かけるのはそのためです。
ビットシフトもフラグ作成に使える
先ほど、
const READ = 1 << 0;
const WRITE = 1 << 1;
と書きました。
左シフトすると、
0001 << 1
↓
0010
になります。
さらに、
0001 << 2
↓
0100
です。
そのため、
const READ = 1 << 0;
const WRITE = 1 << 1;
const DELETE = 1 << 2;
const ADMIN = 1 << 3;
と書くと、
「何番目のbitを使っているか」
がコードから分かりやすくなります。
ビット演算は権限以外でも使われる
ビットフラグの考え方は権限管理だけではありません。
たとえば、
- ステータス管理
- オプション設定
- デバイス制御
- 通信プロトコル
- ファイルフォーマット
- RGBなどの値の抽出
- 組み込みシステム
- ゲームの状態管理
などでも使われます。
特にバイナリデータを扱う場面では、
特定のbitだけ取り出したい
という処理がよくあります。
そこでANDやシフトが活躍します。
実際のビット列を確認すると理解しやすい
ビット演算は10進数だけを見ていると直感的に分かりにくいことがあります。
たとえば、
5 AND 3
だけ見るより、
0101
0011
----
0001
と2進数で見た方が、何が起きているか理解しやすくなります。
AND・OR・XORやシフトの結果を2進数で確認したい場合は、ビット列を実際に確認すると、それぞれの演算結果を比較しやすくなります。
JavaScriptのビット演算には注意点もある
JavaScriptで通常のビット演算子を使う場合、数値は基本的に32bit整数として扱われます。
たとえば、
|
&
^
~
<<
>>
などです。
そのため、大きな整数を扱う場合には注意が必要です。
通常のNumberはより大きな整数も表現できますが、ビット演算へ入ると32bit整数へ変換されます。
大きなビット列を扱う実装では、この仕様を理解しておく必要があります。
算術右シフトと論理右シフト
JavaScriptには、
>>
と、
>>>
があります。
>>は符号を維持する右シフトです。
一方、
>>>はゼロを埋める論理右シフトです。
負の値を扱うと結果が違います。
たとえば、
console.log(-8 >> 1);
// -4
一方、
console.log(-8 >>> 1);
では大きな正の整数になります。
これは負数が2の補数で表現されていることと関係しています。
ビット演算を深く理解していくと、
2進数
16進数
2の補数
Signed / Unsigned
といった話がつながってきます。
まとめ
AND・OR・XORは、単なる計算問題として覚えるより、
複数のON/OFF状態を1つの整数で管理する仕組み
として見ると理解しやすくなります。
基本操作は、
OR
→ フラグを追加する
AND
→ フラグを確認する
AND + NOT
→ フラグを削除する
XOR
→ フラグを反転する
です。
さらに、
1
2
4
8
16
のような2の累乗を使えば、それぞれのbitを独立したフラグとして利用できます。
ビット演算は一見すると低レベルで分かりにくい処理ですが、
「どのbitがONになっているかを見る」
という考え方が分かると、権限管理やステータス管理、通信データなど実際のコードでも読みやすくなります。