Kotlin での、型安全で不変なビットフラグ集合の実装例。
(他の言語でも概ね同様にして型安全なビットフラグ集合を実現できるだろう。)
class Flags private constructor(
private val bits: Int
) {
companion object {
val EMPTY = Flags(0b0000)
val FLAG_A = Flags(0b0001)
val FLAG_B = Flags(0b0010)
val FLAG_C = Flags(0b0100)
val FLAG_D = Flags(0b1000)
}
operator fun plus(flags: Flags): Flags =
Flags(this.bits or flags.bits)
operator fun minus(flags: Flags): Flags =
Flags(this.bits and flags.bits.inv())
operator fun times(flags: Flags): Flags =
Flags(this.bits and flags.bits)
operator fun contains(flags: Flags): Boolean =
this * flags == flags
override fun equals(other: Any?) = when {
this === other -> true
other !is Flags -> false
else -> this.bits == other.bits
}
override fun hashCode() = bits.hashCode()
override fun toString() =
"${Flags::class.java.simpleName}(${::bits.name}=0b${bits.toString(2)})"
}
使用方法
// plus
val ab = Flags.FLAG_A + Flags.FLAG_B // -> 0b0011
val abc = ab + Flags.FLAG_C // -> 0b0111
ab + Flags.FLAG_A // -> 0b0011 既に立っているフラグをさらに立てても影響なし
// minus
val bc = abc - Flags.FLAG_A // -> 0b0110
bc - Flags.FLAG_A // -> 0b0110 既に倒れているフラグをさらに倒しても影響なし
// times
ab * bc // -> 0b0010
// contains
Flags.FLAG_A in ab // -> true
Flags.FLAG_C in ab // -> false
Flags.EMPTY in ab // -> true
ab in abc // -> true 複数のフラグの場合、
ab in bc // -> false 全てが含まれているかどうかを返す
// equals
ab + Flags.FLAG_C == abc // -> true
ab == bc // -> false
解説
上から順に、簡単に解説する。
class
単なる class
にする。
これにより、派生クラスは実装できない。
data class
にはしない。
不正なプロパティ値のインスタンスが他所で生成されないようにする必要があり、そのため後述するようにコンストラクターを private
にするのだが、
data class
にしてしまうと、メンバ関数 copy
が自動的に定義・実装され、それを用いると任意のプロパティ値を持つインスタンスを生成できるため。→https://kotlinlang.org/docs/reference/data-classes.html
private constructor
不正なプロパティ値のインスタンスが他所で生成されないよう、コンストラクターを private
にする。
private val bits: Int
各ビットフラグを保持するビットフィールド。
- 他所から直接参照されないよう
private
にする。 - 不変にするため
val
にする。 - ここでは
Int
を使用しているが、ビットフラグの数が多い場合はLong
やjava.util.BitSet
を使うとよい。1
companion object {
val EMPTY = Flags(0b0000)
val FLAG_A = Flags(0b0001)
val FLAG_B = Flags(0b0010)
val FLAG_C = Flags(0b0100)
val FLAG_D = Flags(0b1000)
}
各ビットフラグの定義を companion object
のメンバ(他の言語でのクラスメンバに当たる)として行う。
いずれのフラグも立っていない状態を表す EMPTY
も忘れず定義しよう。(全てのフラグが立っている状態を表す FULL
もあると便利かもしれない。)
2進数リテラルは接頭辞 0b
をつけることで記述できる。
また、(今回は使用していないが)桁区切りに _
を使用できるので、フラグ数が多い場合は4桁ごとにいれるとよい。例: val FLAG_E = Flags(0b0001_0000)
operator fun plus(flags: Flags): Flags =
Flags(this.bits or flags.bits)
+
演算でフラグを立てられるよう、演算子をオーバーロードする。
operator fun minus(flags: Flags): Flags =
Flags(this.bits and flags.bits.inv())
-
演算でフラグを倒せるよう、演算子をオーバーロードする。
operator fun times(flags: Flags): Flags =
Flags(this.bits and flags.bits)
*
演算で共通のフラグを得られるよう、演算子をオーバーロードする。
operator fun contains(flags: Flags): Boolean =
this * flags == flags
in
演算でフラグが含まれているかどうかを得られるよう、演算子をオーバーロードする。
override fun equals(other: Any?) = when {
this === other -> true
other !is Flags -> false
else -> this.bits == other.bits
}
override fun hashCode() = bits.hashCode()
data class
ではないため、メンバ関数 equals
および hashCode
をオーバーライドする必要がある。
override fun toString() =
"${Flags::class.java.simpleName}(${::bits.name}=0b${bits.toString(2)})"
toString
関数をオーバーライドして、Flags(bits=0x1111)
の形式の文字列に変換できるようにする。 bits
プロパティ値の出力を固定桁数にするのも良いだろう。
/以上
-
クラスを分割することを検討した方が良いとは思うが。
Int
でも32ビットもあるので。 ↩