ビットフィールド
ビット演算の使いどころ
ビット演算の項ではビットの意味や演算子などを説明しました。
ビット演算は慣れないと見た目での意味が分かりづらく、あまり積極的に利用される機能とはいえません。
C言語やC++などでは(特に組み込み系などで)メモリ効率や速度面でのメリットのためにビット演算を使用することもありますが、C#ではそういう用途もないのでほぼ使う機会がないかもしれません。
一応、if文などで使用する「&&」や「||」もビット演算子なので全く使わないということはありませんが、これらをビット演算と意識して使うことはあまりないでしょう。
しかし列挙型を利用したフラグ管理(ビットフィールド)においては使用されることがあります。
標準の.NETライブラリにもビット演算子を使用するものもあります。
フラグ管理(ビットフィールド)
フラグというのは、ある処理の状態や結果を保存しておく変数のことです。
目印として「旗を立てる」ことからフラグ(flag)と呼ばれます。
二種類の状態だけで良い場合、C#ではbool型を使用することが多いでしょう。
(「有効/無効」や「成功/失敗」など)
状態の種類が三種類以上の場合は列挙型を使用します。
例えば以下の列挙型は三種類の状態を扱えます。
//左、真ん中、右の三種類の状態を表す列挙型
enum LCR
{
Left,
Center,
Right
}
自分でコードをイチから書く場合は上記のどちらかでほぼ対応できます。
enumは「複数の状態から、ひとつの状態を選択」することはできます。
では「複数の状態から、いくつかの状態を選択」する場合はどうすればよいでしょうか。
例えば、曜日の中からいくつかの曜日を選択したい場合などです。
//曜日を表す列挙型
enum DayOfWeek
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
方法のひとつとして、enumではなくbool型変数を7個用意する、というのがあります。
(もちろん配列でも良い)
これは単純で分かりやすい方法です。
もう一つ、ビット演算を用いる方法もあります。
それにはまず、列挙型を以下のように書き換えます。
//曜日を表す列挙型
enum DayOfWeekFlags
{
Sunday = 1, //0000_0001
Monday = 2, //0000_0010
Tuesday = 4, //0000_0100
Wednesday = 8, //0000_1000
Thursday = 16, //0001_0000
Friday = 32, //0010_0000
Saturday = 64 //0100_0000
}
列挙型の各要素に、1から順に倍々に値を指定します。
各値を二進数で表すと、桁をひとつずつ繰り上げたものとなります。
(上記コメントを参照)
列挙型の値には「0」を含めておくことが推奨されますが、ビットフィールドとして使用する場合は無くても構いません。
なお、ここでの二進数表記は8ビットで表した場合です。
実際には列挙型は何も指定しなければ32ビット(int型)となります。
この列挙型の各要素をビット演算するとき、二進数の各桁は独立して考えることができます。
例えばSundayとTuesdayの二つを選択したい場合、「1 + 4 = 5」となります。
これを二進数で表すと、
- 00000001 (1)
- 00000100 (4)
- 1 | 4 =
- 00000101 (5)
となります。
(「&演算子(AND)」ではなく「|演算子(OR)」であることに注意)
SundayからSaturdayまでをすべて足した場合でも
- 01111111 (127)
となり、各要素の加算で桁の繰上りが発生することはありません。
一番右端の桁は「Sunday」のオンオフ状態を、その左隣は「Monday」のオンオフ状態を表せます。
つまり「Sunday」の状態を知りたければ、一番右端の桁が「0」か「1」かをチェックするだけで良いことになります。
このようなフラグ管理の方法をビットフィールドといいます。
これを実際のコードにすると以下のようになります。
enum DayOfWeekFlags
{
Sunday = 1, //0000_0001
Monday = 2, //0000_0010
Tuesday = 4, //0000_0100
Wednesday = 8, //0000_1000
Thursday = 16, //0001_0000
Friday = 32, //0010_0000
Saturday = 64 //0100_0000
}
static void Main(string[] args)
{
DayOfWeekFlags dow = 0;
//Sundayフラグをオン
dow |= DayOfWeekFlags.Sunday;
//TuesdayとWednesdayフラグを同時にオン
dow |= DayOfWeekFlags.Tuesday | DayOfWeekFlags.Wednesday;
//二進数を表示
Console.WriteLine(Convert.ToString((int)dow, 2).PadLeft(8, '0'));
//列挙型から各要素を取り出して
//フラグの状態をチェック
foreach(DayOfWeekFlags f in Enum.GetValues(typeof(DayOfWeekFlags)))
Console.WriteLine("{0} = {1}",
f.ToString().PadRight(9),
(dow & f) != 0);
Console.ReadLine();
}
00001101 Sunday = True Monday = False Tuesday = True Wednesday = True Thursday = False Friday = False Saturday = False
列挙型を用いてフラグ管理をする場合、十進数での数値は気にする必要はありません。
列挙型で定義されている要素名に従って操作するだけです。
ビットフィールドの操作
以下にビットフィールドでよく使用されるビット演算子の使い方を示します。
式 | 用途 | 例1 | 例2 | 備考 |
---|---|---|---|---|
a & b | フラグの状態を調べる | 5 & 1 ↓ 00000101 00000001 ↓ 00000001 |
5 & 2 ↓ 00000101 00000010 ↓ 00000000 |
結果が0以外ならオン (調べるビットと同値ならオン) |
a | b | フラグを立てる | 4 | 1 ↓ 00000100 00000001 ↓ 00000101 |
5 | 1 ↓ 00000101 00000001 ↓ 00000101 |
目的のフラグがすでにオンでも問題ない |
a & ~b | フラグを削除 |
5 & ~1 ↓ 00000101 11111110 ↓ 00000100 |
5 & ~2 ↓ 00000101 11111101 ↓ 00000101 |
目的のフラグがすでにオフでも問題ない なお「11111110」は「00000001」の反転 |
a ^ b | フラグを反転 | 5 ^ 1 ↓ 00000101 00000001 ↓ 00000100 |
5 ^ 2 ↓ 00000101 00000010 ↓ 00000111 |
目的のフラグがオンならオフ、オフならオン |
これらのビット演算は標準の.NETライブラリでもちょくちょく使用されます。
例えばファイルの属性を表すFileAttributes列挙型もビットフィールドになっており、属性の取得や設定にビット演算子を使用します。
string path = "test.txt";
//「C:\test.txt」の属性を取得
FileAttributes attrFile = File.GetAttributes(path);
//ReadOnly属性を追加
File.SetAttributes(path, attrFile | FileAttributes.ReadOnly);
bool型変数を複数用意する方法やbool型配列を用いる方法と比べると、ビットフィールドは値型一つのやり取りで済むというメリットがあります。
byte型の列挙型なら1バイトで8個の状態を、int型なら4バイトで32個の状態を表すことができます。
FlagsAttribute
列挙型をビットフィールドとして使用する場合、FlagsAttributeという属性を付けることが推奨されます。
[Flags]
enum DayOfWeekFlags
{
Sunday = 1, //0000_0001
Monday = 2, //0000_0010
Tuesday = 4, //0000_0100
Wednesday = 8, //0000_1000
Thursday = 16, //0001_0000
Friday = 32, //0010_0000
Saturday = 64 //0100_0000
}
列挙型の定義の前の「[Flags]」がFlagsAttribute属性です。
属性とは、データ(クラス、型、メソッドなど)に情報を追加するためのものです。
属性を追加することで、標準では使用できない機能が使用可能になります。
ビットフィールド用列挙型にFlagsAttribute属性を追加する意味はあまりありません。
追加しなくても、すでに示した通りコードは問題なく動作します。
FlagsAttribute属性にすることにデメリットがあるわけでもないので、ビットフィールドであることを明示するためにもつけておいても良いでしょう。
FlagsAttribute属性を追加すると、数値を文字列に変換した時にビットフィールドの組み合わせ文字列に変換されます。
FlagsAttribute属性がない場合は、該当する要素がなければ単に数値(の文字列)に変換されます。
//FlagsAttribute属性なし
enum MyFlagsA
{
A = 1,
B = 2,
C = 4
}
//FlagsAttribute属性あり
[Flags]
enum MyFlagsB
{
A = 1,
B = 2,
C = 4
}
static void Main(string[] args)
{
for (int i = 1; i < 8; i++)
{
Console.WriteLine((MyFlagsA)i);
}
Console.WriteLine();
for (int i = 1; i < 8; i++)
{
Console.WriteLine((MyFlagsB)i);
}
Console.ReadLine();
}
A B 3 C 5 6 7 A B A, B C A, C B, C, A, B, C
HasFlag(.NET Framework4以降)
.NET Framework4以降ならばフラグの状態をチェックするのにHasFlagというメソッドが利用できます。
enum DayOfWeekFlags
{
Sunday = 1, //0000_0001
Monday = 2, //0000_0010
Tuesday = 4, //0000_0100
Wednesday = 8, //0000_1000
Thursday = 16, //0001_0000
Friday = 32, //0010_0000
Saturday = 64 //0100_0000
}
static void Main(string[] args)
{
//SundayとTuesdayをオン
DayOfWeekFlags dow = DayOfWeekFlags.Sunday | DayOfWeekFlags.Tuesday;
Console.WriteLine(dow.HasFlag(DayOfWeekFlags.Sunday));
Console.WriteLine(dow.HasFlag(DayOfWeekFlags.Monday));
Console.WriteLine(dow.HasFlag(DayOfWeekFlags.Sunday | DayOfWeekFlags.Monday));
Console.WriteLine(dow.HasFlag(DayOfWeekFlags.Sunday | DayOfWeekFlags.Tuesday));
Console.ReadLine();
}
True False False True
HasFlagメソッドは指定の値がオンかオフかをチェックできます。
(戻り値はbool型)