C++では、あらかじめ定義しておいた定数をOr演算で組み合わせて使用するという方法がよく用いられています。
#include <iostream>
using namespace std;
#define DIRECTION_NORTH 0x00000001
#define DIRECTION_WEST 0x00000002
#define DIRECTION_SOUTH 0x00000004
#define DIRECTION_EAST 0x00000008
#define DIRECTION_NONE 0x00000000
int main()
{
int d;
d = DIRECTION_NORTH | DIRECTION_WEST;
if ( d & DIRECTION_NORTH ) cout << "North";
if ( d & DIRECTION_WEST ) cout << "West";
if ( d & DIRECTION_SOUTH ) cout << "South";
if ( d & DIRECTION_EAST ) cout << "East";
cout << endl;
return 0;
}
.NET Frameworkでも、列挙体定数に対してこの例と同じようにOr演算による組み合わせの操作を行えるようになっています。 .NET Frameworkでは、このような操作を行う列挙体に対してFlagsAttributeを適用することで、列挙体の値がフラグの組み合わせとして使用できることを明示出来るようになっています。
Flags属性を適用した列挙体
Flags属性は列挙体に対して適用し、その列挙体の各定数が組み合わせ可能なフラグであることを明示します。 早速この属性を用いた例を見てみたいと思います。
using System;
// 方角を表す列挙体
[Flags]
enum Direction : int {
North = 0x00000001,
West = 0x00000002,
South = 0x00000004,
East = 0x00000008,
None = 0x00000000,
}
class Sample {
static void Main()
{
Direction d = Direction.North | Direction.West;
// 各定数とのAndをとってその定数が指定されているかを確かめる
if ((d & Direction.North) != Direction.None) Console.Write("North");
if ((d & Direction.West ) != Direction.None) Console.Write("West");
if ((d & Direction.South) != Direction.None) Console.Write("South");
if ((d & Direction.East ) != Direction.None) Console.Write("East");
Console.WriteLine();
// d の値を直接表示
Console.WriteLine(d);
}
}
Imports System
' 方角を表す列挙体
<Flags> Enum Direction As Integer
North = &h1
West = &h2
South = &h4
East = &h8
None = &h0
End Enum
Class EnumSample
Shared Sub Main()
Dim d As Direction = Direction.North Or Direction.West
' 各定数とのAndをとってその定数が指定されているかを確かめる
If (d And Direction.North) <> Direction.None Then Console.Write("North")
If (d And Direction.West ) <> Direction.None Then Console.Write("West")
If (d And Direction.South) <> Direction.None Then Console.Write("South")
If (d And Direction.East ) <> Direction.None Then Console.Write("East")
Console.WriteLine()
' d の値を直接表示
Console.WriteLine(d)
End Sub
End Class
NorthWest North, West Press any key to continue
実行結果を見ると、先ほどのC++のコードと同様の操作を行えるようになっていることがわかります。 ただ、if文でAnd演算によって定数が指定されているかを確かめる場合、C++では条件式の値が0以外であれば真とされたのに対し、C#/VB.NETでは条件式の値はbool型とならなければならないので、C++のように簡潔な記述はできなくなっています。 そのため、値が0で何も指定されていないことを示すDirection.Noneメンバを用意しておき、これと比較する必要があります。
Flags属性を適用すべき/すべきでない場合
Flags属性を適用すれば定数を組み合わせることを明示することはできますが、方角を表すDirection型でこのような組み合わせを可能にするのは不適当でないかと考えられます。 なぜなら次のコードのように、北と西を組み合わせた場合は北西を表すようにすることはできますが、同時に北と南のフラグを組み合わせることも可能になるからです。
using System;
// 方角を表す列挙体
[Flags]
enum Direction : int {
North = 0x00000001,
West = 0x00000002,
South = 0x00000004,
East = 0x00000008,
None = 0x00000000,
}
class Sample {
static void Main()
{
Direction d;
// 北と西を組み合わせることで、北西を表すことはできる
d = Direction.North | Direction.West;
// ただし、方角を表すのに、北と南が同時に指定できるのは不適切
d = Direction.North | Direction.South;
// それでも、単なる値としては有効
Console.WriteLine(d);
}
}
Imports System
' 方角を表す列挙体
<Flags> Enum Direction As Integer
North = &h1
West = &h2
South = &h4
East = &h8
None = &h0
End Enum
Class EnumSample
Shared Sub Main()
Dim d As Direction
' 北と西を組み合わせることで、北西を表すことはできる
d = Direction.North Or Direction.West
' ただし、方角を表すのに、北と南が同時に指定できるのは不適切
d = Direction.North Or Direction.South
' それでも、単なる値としては有効
Console.WriteLine(d)
End Sub
End Class
このように、単なる値としては有効ですが、意味としては無効という矛盾した値が作成されてしまう可能性があります。 よって、列挙体で北西のような値を表したい場合は、Flags属性を適用した列挙体を使用してOrによって組み合わせるのではなく、Flags属性を適用していない列挙体を使用し、新たに専用のメンバを増やした方が適切であると考えられます。
// 方角を表す列挙体
enum Direction : int {
North = 1,
NorthWest = 2,
West = 3,
SouthWest = 4,
South = 5,
SouthEast = 6,
East = 7,
NorthEast = 8,
None = 0,
}
' 方角を表す列挙体
Enum Direction As Integer
North = 1
NorthWest = 2
West = 3
SouthWest = 4
South = 5
SouthEast = 6
East = 7
NorthEast = 8
None = 0
End Enum
この方法では北北西を表す必要に迫られた場合にはさらにメンバを追加しなければならないと言う不便性もありますが、「東と北西と北北東」のような無効といえる組み合わせがなされる可能性がないことと比較するとそれほど問題にならないと思われます。
整理すると、Flags属性を指定するべきかどうかの判断基準は次のようになります。
- Flags属性を指定すべき場合
- チェックボックスのように複数の項目を同時に選択することができる場合。
組み合わせることによって無効な値(事象)が現れないようなものを列挙体で表現する場合。 - Flags属性を指定すべきでない場合
- ラジオボタンのように常に一つだけの項目しか選択できない場合。
組み合わせると無効な組み合わせが生じるものやそもそも組み合わせる必要性のないものを列挙体で表現する場合。
では、Flags属性を指定すべき列挙体にはどのようなものがあるのか、その例を見てみたいと思います。 ひとつの例として、ファイルの属性があります。 ファイルの属性には「読み取り専用、隠し、アーカイブ、システム」などの属性が存在します。 これらの属性は組み合わせることが可能で、なおかつ組み合わせによっても無効な値が生じません。
次の例は、ファイルの属性を表す列挙体であるFileAttributes列挙体を実際に作成して使用しています。 何も値が指定されていない場合、つまり属性が指定されていない場合は通常のファイルであることを示すNormalを取るようにしています。
using System;
// ファイルの属性を表す列挙体
[Flags]
enum FileAttributes : int {
ReadOnly = 0x00000001,
Hidden = 0x00000002,
Archive = 0x00000004,
System = 0x00000008,
Normal = 0x00000000,
}
class Sample {
static void Main()
{
// faの値はファイルから取得したとする
FileAttributes fa = FileAttributes.Hidden | FileAttributes.System;
Console.WriteLine("ファイルの属性は、{0} です。", fa);
}
}
Imports System
' ファイルの属性を表す列挙体
<Flags> Enum FileAttributes As Integer
[ReadOnly] = &h1
Hidden = &h2
Archive = &h4
System = &h8
Normal = &h0
End Enum
Class EnumSample
Shared Sub Main()
' faの値はファイルから取得したとする
Dim fa As FileAttributes = FileAttributes.Hidden Or FileAttributes.System
Console.WriteLine("ファイルの属性は、{0} です。", fa)
End Sub
End Class
ファイルの属性は、Hidden, Systemです。 Press any key to continue
ちなみに、この例で使用したFileAttributes列挙体と同様の機能を持った列挙体がSystem.IO名前空間に存在します(System.IO.FileAttributes)。 こちらのFileAttributes型はこの例で使用したものよりも様々な属性を表すことができるようになっています。
Flags属性を適用した列挙体のガイドライン
Flags属性を適用する場合、各メンバにどのような値を指定すべきか、またどのような名前にすべきか、などのガイドラインがEnumクラスおよびFlagsAttributeクラスでまとめられています。