ビット演算でフラグをまとめる
このレッスンで分かること
- 独立した「はい」「いいえ」が並ぶ設定を、1 つの数値にまとめられます
|で立てる、& ~で降ろす、^で切り替える、&で調べる
真偽値を 3 つ持ち回るのがつらい
プロフィールサイトの通知設定に「メール」「プッシュ」「音」の 3 つがあるとします。素直に書けばこうなります。
JavaScript
const settings = { mail: true, push: false, sound: true };これで困らない場面も多くあります。困るのは、この 3 つを引数で渡したり、保存したり、あとから前の状態と比べたりするときです。どこへ行くにも 3 つセットで持ち回ることになり、「今どの組み合わせなのか」を 1 つの値として言い表せません。
そこで、3 つに別々の数値を割り当てて、足し合わせた 1 つの数値で持ちます。
JavaScript
const MAIL = 1;
const PUSH = 2;
const SOUND = 4;1、2、4 と倍にしていくのが決まりです。こうすると、どの組み合わせを足しても結果が重複しません。5 なら 1 と 4、つまりメールと音だけが有効だと一意に決まります。3 つ全部なら 7 です。この数値をフラグと呼びます。
なぜ倍々なのかは、数値を 2 進で見ると位が 1 つずつずれるからです。ただし、ここで 2 進の変換を覚える必要はありません。「1 から始めて倍にしていく」「足しても混ざらない」の 2 つだけ持っていれば、この先のコードは全部読めます。
4 つの演算子で足りる
フラグを操作する演算子は 4 つです。どれも数値を返します。
JavaScript
let enabled = MAIL; // 1
enabled = enabled | PUSH; // 立てる 3
enabled = enabled & ~PUSH; // 降ろす 1
enabled = enabled ^ SOUND; // 切り替える 5
console.log(enabled & MAIL); // 調べる 1
console.log(enabled & PUSH); // 立っていない 0| やりたいこと | 書きかた | 結果 |
|---|---|---|
| 立てる | enabled | PUSH | 既に立っていても変わらない |
| 降ろす | enabled & ~PUSH | 立っていなくても変わらない |
| 切り替える | enabled ^ PUSH | 呼ぶたびに反転する |
| 調べる | enabled & PUSH | 立っていればその値、なければ 0 |
| は「どちらかで立っていれば立てる」なので、既に立っているところに重ねても何も起きません。~ は全部の位をひっくり返す演算子で、~PUSH は「プッシュ以外」を表します。それと & を取ると、プッシュだけが落ちて他は残ります。降ろすときは & ~ の 2 文字で 1 組と覚えてください。^ は「片方だけ立っているときに立てる」ので、同じフラグに 2 回使うと元に戻ります。押すたびに切り替わるボタンにちょうど合います。
調べた結果は true ではない
ここがいちばん事故の多いところです。& が返すのは、立っていればそのフラグの値、立っていなければ 0 という数値です。
JavaScript
if ((enabled & PUSH) === true) { // 決して通らない
}
if (enabled & PUSH) { // 正しい。0 は falsy、それ以外は truthy
}
const push = (enabled & PUSH) !== 0; // true か false が欲しいならこう書くif の条件に直接置くぶんには、0 が falsy であることに任せて問題ありません。戻り値やオブジェクトのプロパティに入れるときは !== 0 を付けて変換します。
カッコにも注意が要ります。& は === より優先順位が低いので、enabled & PUSH === 2 は enabled & (PUSH === 2) と解釈されます。比較と混ぜるときは必ずカッコで囲んでください。
なおフラグが効くのは、それぞれが独立していて同時に成り立つ場合だけです。「下書き」「公開」「非公開」のように排他な状態は、文字列 1 つで持つほうが素直です。
よくある間違い
+で立てる —3 + 2は5になり、プッシュが降りて音が立ったことになります。3 | 2なら3のままです- 降ろすつもりで
^を使う —^は切り替えなので、立っていなければ逆に立ってしまいます。今の状態に関係なく降ろすなら& ~です &&と&を書き間違える —enabled && PUSHはPUSHが常に2なので、enabledが0以外なら必ず通ります。1 文字違いで、常に真になる条件ができあがります
要件
- 関数名は applyBadge。引数は flags (現在の数値)、action (操作の種類)、badge (対象のフラグ) の 3 つ
- action が on なら立てる、off なら降ろす、toggle なら切り替える。それ以外の action では flags を変えない
- 戻り値は { flags, labels }。flags は適用後の数値
- labels は立っているオプション名を published, featured, new の順に並べた配列。1 つも立っていなければ空配列