twitterでこんな定義をネタにしてましたが、「じゃあどうすればいいのか」という話もしておこうかと思います。
//アンチパターン
enum Hoge{
A = 1,
B = 2,
C = 3,
D = 4,
AB = 12,
CD = 34
}
このコードの駄目な点は
- 「BC」のような要素が欲しいとき、新たに定義を書く必要がある。
- 複合要素の値と単要素との差を、文字列としてしか判断できない
- 複合要素内に、ある1要素を含んでいるかどうかを調べるのが面倒
- 要素の値に一意性が無い(例:CはA+Bの値と同じなので、ABの複合フラグとの差異が取りにくい)
といった箇所です。
複数の要素を「AB」や「CD」のように同時に取りたい場合、下記3+1点に注意します。
- 各要素の値を、ビットフラグとして考える
- 複合要素には各要素の論理和を取る(例:AとBの複合フラグの時は「A | B」)
- Flags属性を付ける
- そもそも複合要素を定義しない(ただし状況による)
4はちょっと特殊な話になりますので、1-3を踏まえ、列挙の定義をこのように修正します。
//修正その1
[Flags]
enum Hoge{
A = 1,
B = 2
C = 4,
D = 8,
AB = A | B,
CD = C | D
}
##解説
####1.各要素の値を、ビットフラグとして考える
複数の要素を同時に表現するためには、列挙の各要素を
ビットフラグとして考えるべきです。つまり、2進数で表した時
要素 | 2進数 | 10進数 |
---|---|---|
A | 0001 | 1 |
B | 0010 | 2 |
C | 0100 | 4 |
D | 1000 | 8 |
AとB | 0011 | 3 |
CとD | 1100 | 12 |
となるよう定義すれば、複数の値を取ったとしても、他の要素の値と重複することが無いため、
今後E、F…と要素が増えても安定した値を保つことができます。
####2.複合要素には各要素の論理和を使う
修正後のソースは
AB = A | B
と定義しましたが、
AB = 3
と定義しても同じ意味を持ちます。
しかし前者の方が可読性に勝りますし、値の計算ミスも避けることができますので、
リテラルで書く必要がない箇所は、リテラルを書かないようにしましょう。
####3.Flags属性を付ける
.net系言語には属性というものがあり、それを付加することでプログラムの挙動を変えることが可能です。
修正後のソースに付加したFlags属性は、列挙変数の出力文字列を、フラグ表示に適した形に整えてくれます。
Flags属性が無い場合、各要素の値の定義があれば、その要素名が表示され、なければ数値として表示されます。
//Flags属性なし
Console.WriteLine(Hoge.AB); // 「AB」が出力される
Console.WriteLine(Hoge.AB | Hoge.C); // 「7」が出力される
Flags属性を付加した場合は、常に各要素の論理和が判断され、その結果を出力してくれます。
//Flags属性あり
Console.WriteLine(Hoge.AB); // 「AB」が出力される
Console.WriteLine(Hoge.AB | Hoge.C); // 「AB, C」が出力される
Console.WriteLine(Hoge.A | Hoge.B | Hoge.C); // これも「AB, C」と出力される
####4.そもそも複合要素を定義しない
上記の「修正その1」にはもう一つ問題点があります。
この列挙の定義は、AB、CDという複合フラグには対応していますが、
今後「AC」や「BD」、はたまた「ABCD全部」といった値が欲しくなった時、
再度このソースを修正しなければいけないということです。
こういった複合フラグについては、列挙の定義に含ませるのは、
汎用性を考えるとあまり好ましくないと思いますので、
どうしても、という理由がなければ、
下記のように変数に直接入れてしまうのが良いと思われます。
//修正その2
[Flags]
enum Hoge{
A = 1,
B = 2,
C = 4,
D = 8
}
//使用するとき
var AB = Hoge.A | Hoge.B;
var CD = Hoge.C | Hoge.D;
var ABCD = Hoge.A | Hoge.B | Hoge.C | Hoge.D;
####列挙をビットフラグとして扱った時の強み
各要素の和が、複合要素の合計値と重複しない場合、
「複合要素の値と1要素との論理積」と、「その1要素」を比較することで、
複合要素内に、その要素が含まれているかを調べることができます。
これを利用し、下記のようなコードを書くことで、フラグの存在チェックを行うことができます。
アンチパターンにあるような定義の仕方だと、
「要素の値を文字列化し、桁を区切って、文字位置を検索して…」
といった処理が必要になり、非常に面倒です。
//要素を持っているかを調べる
var ab = Hoge.AB;
var cd = Hoge.CD;
bool HasC; // Cの要素を保持しているか
HasC = (ab & Hoge.C) == Hoge.C; // false
HasC = (cd & Hoge.C) == Hoge.C; // true
また、.net4からは、列挙にHasFlagというメソッドが追加されていますので、
HasC = ab.HasFlag(Hoge.C); // false
といった記述が可能になり、自前でメソッドを用意しなくてよくなっています。