TypeScriptでビットフラグざっくり解説
はじめに
実務で、ある機能をユーザーが利用できるかどうかの判定にビットフラグを使うことになったのですが、会議に出ていても???しか頭に浮かばず、、、
これは流石にまずいと思い、実際に自分で手を動かして確認しました!(UIの実装はサボってます😇)
ビットフラグとは?
ビットフラグ(Bitwise flags) は、一つの整数値の各ビットを ON/OFFのスイッチ とみなし、特定の使用法や機能を有効にしたり無効にしたりするのに使用できるものです。
MDN でも「複数のフラグを 1 つの値にエンコードでき、ビットレベルで高速に扱える」と説明されています。(参照)
なぜビットフラグを使う?
複数の状態を管理する必要があるとき、通常は true / false
のフラグをそれぞれ用意して管理する方法がよくあると思います。(自分の関わったプロジェクトでもほとんどそうでした)
しかしこの方法だと、最初は良くてもプロジェクトが大きくなるにつれて、どんどん管理する状態が増えて管理が複雑になってしまいます。
ここで有効なのが ビットフラグ です。
仕組み
例えばRPGを作っていて、キャラクターが持つ能力を「フラグ」として管理したいと思います。
キャラクターは複数の能力を持つことができます。
能力名 | フラグの値(16進) | 10進数 |
---|---|---|
攻撃可能 | 0x0001 |
1 |
防御可能 | 0x0002 |
2 |
魔法使用可能 | 0x0004 |
4 |
飛行可能 | 0x0008 |
8 |
これをCharacterAbility
というオブジェクトで定義しておきます。
const Ability = {
ATTACK: 0x0001, // 0001
DEFEND: 0x0002, // 0010
MAGIC: 0x0004, // 0100
FLY: 0x0008, // 1000
};
キャラクターに能力を設定
Aというキャラクターが「攻撃」「魔法」「飛行」能力を持っていたとします。その場合はこう記述できます。
const myCharacterAbilities = Ability.ATTACK | Ability.MAGIC | Ability.FLY;
console.log(myCharacterAbilities); // → 13 (1 + 4 + 8)
能力があるかどうかを判定
「このキャラは魔法が使える?」を判定するにはこんな感じで判定できます。
// myCharacterAbilities = 0x0D(13 = ATTACK + MAGIC + FLY)
// Ability.MAGIC = 0x04
//
// 0x0D
// AND 0x04
// -------------
// 0x04 → 0 ではないので「魔法 ON」
if (myCharacterAbilities & Ability.MAGIC) {
console.log("このキャラは魔法が使えます!");
}
myCharacterAbilities & Ability.MAGIC
が 0 以外なら、そのフラグが含まれている(=魔法が使える)、という意味になります。
実際に動かしてみる
では先ほどのRPGゲームをもとに、実際にソースコード上で動かしてみます。
準備
今回は vite
を使っていきます(使ったことなかったのでついでに使ってみたかった)。
npm create vite@latest bitflag-sample -- --template vanilla-ts
cd bitflag-sample
npm install
フラグとユーティリティ関数を作成
キャラクターの能力と、それらを設定・更新する関数をそれぞれ定義します。
/** キャラが持ち得る能力を 1bit = 1能力 で表現 */
export const Ability = {
ATTACK: 0x0001, // 0001
DEFEND: 0x0002, // 0010
MAGIC: 0x0004, // 0100
FLY: 0x0008, // 1000
} as const;
/** Ability のいずれか 1 つを表す型 */
export type Ability = typeof Ability[keyof typeof Ability];
import { Ability } from './ability'
/** 指定した能力フラグをセット(ON) */
export const set = (f: number, flag: Ability) => f | flag
/** 指定した能力フラグをクリア(OFF) */
export const clear = (f: number, flag: Ability) => f & ~flag
/** 指定した能力フラグをトグル(ON/OFF) */
export const toggle = (f: number, flag: Ability) => f ^ flag
/** 指定した能力フラグがセットされているか? */
export const has = (f: number, flag: Ability) => (f & flag) !== 0
単体テスト
ビットフラグの関数がどう動くか、vitest
を使って確認してみます。
npm i -D vitest @vitest/ui
import { describe, expect, it } from 'vitest'
import { Ability } from './ability'
import { clear, has, set, toggle } from './bitflag'
describe('bitflag', () => {
it("set / has", () => {
let f = 0
// 攻撃をセット
f = set(f, Ability.ATTACK)
// 攻撃だけがセットされていることを確認
expect(has(f, Ability.ATTACK)).toBe(true)
expect(has(f, Ability.DEFEND)).toBe(false)
})
it('clear', () => {
// 防御と魔法をセット
let f = Ability.DEFEND | Ability.MAGIC
// 防御をクリア
f = clear(f, Ability.DEFEND)
// 防御がクリアされていることを確認
expect(has(f, Ability.DEFEND)).toBe(false)
expect(has(f, Ability.MAGIC)).toBe(true)
})
it('toggle', () => {
let f = 0
// 飛行をトグル
f = toggle(f, Ability.FLY)
expect(has(f, Ability.FLY)).toBe(true)
// 再度トグルして飛行をOFFにする
f = toggle(f, Ability.FLY)
expect(has(f, Ability.FLY)).toBe(false)
})
})
これを使ってテストしてみます。
"scripts": {
"test": "vitest --ui"
},
無事にテストがパスしました🥳
まとめ
ビットフラグを使えば、複数の状態をシンプルに管理できるんだなーということが理解できました!
一方で、例えばバックエンドでビットフラグで管理した値をフロントエンドに渡す時などに、「この値ってどういう状態?」ってことが起きそうで、実装難易度が少し高いのかな?とも思いました(より良い使い方あったらぜひ教えてください🙇!)
Discussion