ビット演算でフラグをまとめる

このレッスンで分かること

  • 公開・注目・新着のような、はい/いいえの設定を 1 つの数値にまとめられます
  • | で立てる、& ~ で降ろす、^ で切り替える、& で調べる、の 4 つで足ります
  • 判定の結果は true ではなく数値なので、=== true と比べてはいけません

ビット演算でフラグをまとめる とは

作品カードの表示オプションのように、独立した「はい/いいえ」がいくつも並ぶ設定を 1 つの数値で持ち、演算子で立て降ろしする書きかたです。

boolean を 3 つ持つのをやめる

作品カードに「公開」「注目」「新着」の 3 つの表示オプションを付けたいとします。素直に書けばこうなります。

const work = { title: "プロフィールサイト", published: true, featured: false, isNew: true };

これで何も問題は起きません。実際、多くの場面ではこれが正解です。ただし、オプションが増えるほど関数の引数が長くなり、「今どの組み合わせなのか」を 1 つの値として渡したり保存したりできません。そこで、3 つのオプションに別々の数値を割り当てて、足し合わせた 1 つの数値で持つ方法があります。

const PUBLISHED = 1; const FEATURED = 2; const NEW = 4;

1、2、4 と倍々にするのが決まりです。こうすると、どの組み合わせを足しても結果が重複しません。5 なら 1 と 4、つまり公開かつ新着だと一意に決まります。3 つ全部なら 7 です。この数値のことをフラグと呼びます。

flags公開注目新着
0いいえいいえいいえ
1はいいいえいいえ
3はいはいいいえ
5はいいいえはい
7はいはいはい

なぜ倍々なのかは、数値を 2 進で見ると位が 1 つずつずれるからです。ただし、ここで 2 進の変換を覚える必要はありません。「1 から始めて倍にしていく」「足しても混ざらない」の 2 つだけ持っていれば、この先のコードは全部読めます。

4 つの演算子で立て降ろしする

フラグを操作する演算子は 4 つで足ります。どれも数値を返します。

let flags = PUBLISHED; // 1 flags = flags | FEATURED; // 立てる 3 flags = flags & ~FEATURED; // 降ろす 1 flags = flags ^ NEW; // 切り替え 5 const isPublished = flags & PUBLISHED; // 調べる 1

| は「どちらかで立っていれば立てる」なので、既に立っているところに重ねても何も起きません。何度呼んでも結果が同じというのは、実は扱いやすい性質です。

~ は全部の位をひっくり返す演算子で、~FEATURED は「注目以外」を表します。それと & を取ると、注目だけが落ちて他は残ります。降ろすときは & ~ の 2 文字で 1 組と覚えてください。

^ は「片方だけ立っているときに立てる」ので、同じフラグに 2 回使うと元に戻ります。押すたびに切り替わるボタンの実装にちょうど合います。

& で調べたときに返るのは true ではなく、立っていればそのフラグの値、立っていなければ 0 です。

console.log(5 & PUBLISHED); // 1 console.log(5 & FEATURED); // 0 console.log(5 & NEW); // 4
やりたいこと書きかた結果
立てるflags | FEATURED既に立っていても変わらない
降ろすflags & ~FEATURED立っていなくても変わらない
切り替えるflags ^ FEATURED呼ぶたびに反転する
調べるflags & FEATURED立っていればその値、なければ 0

まとめて立てる、全部降ろす

| でつないだ値もフラグとして扱えます。よく使う組み合わせに名前を付けておくと、呼び出し側が読みやすくなります。

const ALL = PUBLISHED | FEATURED | NEW; // 7 const PICKUP = PUBLISHED | FEATURED; // 3 let flags = 0; flags = flags | PICKUP; // 公開と注目を一度に立てる 3 flags = flags & ~ALL; // 全部降ろす 0

複数のフラグをまとめて調べたいときは、目的によって書きかたが変わります。

const hasAny = (flags & PICKUP) !== 0; // どちらかが立っている const hasAll = (flags & PICKUP) === PICKUP; // 両方立っている

「どちらか」は 0 でないこと、「両方」は取り出した結果が元の組み合わせと一致すること、で判定します。この 2 つを取り違えると、条件がゆるすぎたり厳しすぎたりする不具合になります。

判定の結果を true と比べない

ここがいちばん事故の多いところです。& の結果は数値なので、=== true は絶対に成り立ちません。

if ((flags & FEATURED) === true) { // 決して通らない } if (flags & FEATURED) { // 正しい。0 は falsy、それ以外は truthy } const featured = (flags & FEATURED) !== 0; // boolean が欲しいならこう書く

if の条件に直接置くぶんには、0 が falsy であることに任せて問題ありません。関数の戻り値やオブジェクトのプロパティに入れるときは、!== 0 を付けて truefalse に変換してから渡します。

もう 1 つ、カッコを忘れると意図が変わります。&=== より優先順位が低いので、flags & FEATURED === 2flags & (FEATURED === 2) と解釈されます。比較と混ぜるときは必ずカッコで囲んでください。

フラグの立て降ろしを 1 か所の関数に集めるのが定石です。|& ~ が画面のあちこちに散らばると、どこで何が変わったのか追えなくなります。「flags を受け取って新しい flags を返す関数」を 1 本用意し、外からはその関数だけを呼ぶ形にします。第3章の純粋関数の考えかたがそのまま当てはまります。

いつ使い、いつ使わないか

フラグは万能ではありません。使いどころを外すと、読みにくいだけのコードになります。

// 向いている // 独立したはい/いいえが 3 つ以上あり、組み合わせを 1 つの値として渡したい // 向いていない // 選択肢が「下書き」「公開」「非公開」のように排他なとき。文字列 1 つで足りる

排他な状態は status という文字列 1 つで表すほうが、後から状態を足すときも読むときも素直です。フラグが効くのは、それぞれが独立していて同時に成り立つ場合だけです。

なお、JavaScript のビット演算は 32 ビットの整数として扱われます。フラグを 31 個より多く並べる設計は避けてください。実務でそこまで増えることは、まずありません。

よくある間違い

  • + で立てる
flags = flags + FEATURED; // 既に立っていると 2 重に足されて壊れる flags = flags | FEATURED; // 正しい

3 + 25 になり、注目が降りて新着が立ったことになります。3 | 2 なら 3 のままです。

  • 降ろすつもりで ^ を使う
flags = flags ^ FEATURED; // 立っていなければ、逆に立ってしまう flags = flags & ~FEATURED; // 確実に降ろす

^ は切り替えです。今の状態に関係なく降ろしたいなら & ~ を使います。

  • &&& を書き間違える
if (flags && FEATURED) { // FEATURED は常に 2 なので、flags が 0 以外なら必ず通る } if (flags & FEATURED) { // 正しい }

1 文字違いで、常に真になる条件ができあがります。フラグを扱っているファイルでは特に見落としやすいので、書いたあとに読み返してください。

要件

  1. 関数名は applyBadge。引数は flags (現在の数値)、action (操作の種類)、badge (対象のフラグ) の 3 つ
  2. action が on なら立てる、off なら降ろす、toggle なら切り替える。それ以外の action では flags を変えない
  3. 戻り値は { flags, labels }。flags は適用後の数値
  4. labels は立っているオプション名を published, featured, new の順に並べた配列。1 つも立っていなければ空配列

入出力例

applyBadge(1, "on", 2){"flags":3,"labels":["published","featured"]} applyBadge(7, "off", 4){"flags":3,"labels":["published","featured"]} applyBadge(3, "toggle", 2){"flags":1,"labels":["published"]} applyBadge(5, "resize", 2){"flags":5,"labels":["published","new"]} applyBadge(0, "off", 1){"flags":0,"labels":[]}

ヒント

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部
main.js
main.js
学習モード

メモ

ビット演算でフラグをまとめる

⌘S で保存