PHPでビット演算フラグ管理!1つの整数に複数の状態をまとめちゃおう

こんにちは、さるまりんです。

検索ソリューションやデータベースを触っていると、「int フィールドが少ない!boolean をバラで持つのはもったいない!」そんな場面に出会ったことはありませんか?

そこで思いついたのが ビット演算によるフラグ管理
1つの整数にフラグを束にして、0/1 の状態をたくさん詰め込んでしまおうという作戦です。

フラグ設計の基本

フラグには2の累乗を割り当てます。

<?php
class Flags {
    const IS_ACTIVE      = 1 << 0; // 1
    const IS_PRIVATE     = 1 << 1; // 2
    const HAS_IMAGE      = 1 << 2; // 4
    const IS_FEATURED    = 1 << 3; // 8
    const ALLOW_COMMENTS = 1 << 4; // 16
}

こうすると1,2,4,8,16...と重ならないフラグができます。

基本操作(set / remove / check)

操作にはビット演算子を使います。

<?php
// フラグを追加します
function addFlag(int $state, int $flag): int {
    return $state | $flag;
}

// フラグを削除します
function removeFlag(int $state, int $flag): int {
    return $state & (~$flag);
}

// フラグが立ってるかチェックします
function hasFlag(int $state, int $flag): bool {
    return ($state & $flag) !== 0;
}

実験してみます!

以下のようなPHPプログラムで実行してみたいと思います。

<?php
require_once "Flags.php"; // 上のフラグの定義と関数を読み込む

// 状態を保持する変数です。
$state = 0;

// フラグを追加
$state = addFlag($state, Flags::IS_ACTIVE);
$state = addFlag($state, Flags::HAS_IMAGE);
echo $state . PHP_EOL; // => 5(1 + 4)

// フラグ確認
var_dump(hasFlag($state, Flags::IS_ACTIVE));   // true
var_dump(hasFlag($state, Flags::IS_FEATURED)); // false

// フラグ削除
$state = removeFlag($state, Flags::HAS_IMAGE);
echo $state . PHP_EOL; // => 1

ターミナルで走らせると「なるほど〜」ってなるやつです。

こちらがその出力です。

% php runner.php 
5
bool(true)
bool(false)
1

DBと組み合わせる

MySQL などデータベースではINTBIGINTにそのまま保存できます。
検索もシンプルです。

-- HAS_IMAGE フラグが立っている行
SELECT * FROM items
WHERE (flags & 4) <> 0;

-- ACTIVE かつ HAS_IMAGE が両方立っている行(AND条件)
SELECT * FROM items
WHERE (flags & (1 | 4)) = (1 | 4);

-- ACTIVE または HAS_IMAGE のいずれかまたは両方が立っている行(OR条件)
SELECT * FROM items
WHERE (flags & (1 | 4)) <> 0;

まとめるとこうです。

-- OR条件(ANY)
(flags & mask) <> 0

-- AND条件(ALL)
(flags & mask) = mask

-- NONE条件(NOT)
(flags & mask) = 0

よく使う条件であればアプリ側でマスク値を組み立てて、SQLを準備しておいてあげると使いやすいですね。

注意点

  • PHPのintは環境によって 32bit / 64bit が異なる
  • 符号ビットを使うと負の数になるので注意
  • DB検索ではインデックスが効きにくい → 頻出フラグは別カラムにするのも良いかも
  • 1,2,4,8… の数字はそのまま使わず定数名で管理する

こんな風に一つの整数にいくつものフラグを入れて状態管理できます。真偽値で表せる属性はまとめて入れておけるの効率良いですね。メモリは昔に比べて安くなりましたが、少しでも節約できるところはしたい。通信コストもそうですよね。頭を使ってうまく使っていきたいです。

読んでくださってありがとうございました。

それではまた!