42
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

enumで複数の要素を持たせるときのアンチパターンとその解決法

Last updated at Posted at 2013-02-05

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点に注意します。

  1. 各要素の値を、ビットフラグとして考える
  2. 複合要素には各要素の論理和を取る(例:AとBの複合フラグの時は「A | B」)
  3. Flags属性を付ける
  4. そもそも複合要素を定義しない(ただし状況による)

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

といった記述が可能になり、自前でメソッドを用意しなくてよくなっています。

42
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?