ここでは列挙体の基本とその操作について、また列挙体の暗黙の基底クラスであるEnumクラスについて見ていきます。

列挙体を用いると関連のある一連の数値定数をひとまとめに扱うことができます。 .NET Frameworkにおいては、列挙体は個々の言語に特有な機能としてではなく、フレームワークでその基本的な機能が提供されています。 なお、列挙型という用語が用いられる場合がありますが、ここでは.NET Frameworkのドキュメントに合わせて列挙体に統一します。

§1 列挙体の基本

列挙体の様々な機能を説明するのに先立って、もっとも基本的な列挙体の使い方の例を挙げてみます。

列挙体の宣言と使用例
using System;

// 方角を表す列挙体
enum Direction {
  North,
  West,
  South,
  East,
}

class Sample {
  static void Main()
  {
    Direction d = Direction.North;

    if (d == Direction.North) Console.WriteLine( "d は北向きです" );
  }
}

実行結果については省略しますが「d は北向きです」と表示されるはずです。 この例のように、東西南北という一連の方角を表す定数は、それぞれバラバラに存在するよりも一つのグループとして存在しているべきです。

もちろん、何らかの理由で単なる定数としてあつかった方がよい場合もありますが、一方で、列挙体の場合は値としても名前(つまり値の意味)としても表記でき、それらを結びつけておくことが出来ます。 定数では、定数として与えられているその値しか表記できません。

列挙体定数の値と値の意味
using System;

// 方角を表す列挙体
enum Direction {
  North,
  West,
  South,
  East,
}

class Sample {
  static void Main()
  {
    Direction d = Direction.North;

    Console.WriteLine("d の方角は {0} です。", d);
    Console.WriteLine("d の持つ値は {0} です。", (int)d);

    Console.WriteLine("Direction.North の持つ値は {0} です。", (int)Direction.North);
    Console.WriteLine("Direction.West の持つ値は {0} です。", (int)Direction.West);
    Console.WriteLine("Direction.South の持つ値は {0} です。", (int)Direction.South);
    Console.WriteLine("Direction.East の持つ値は {0} です。", (int)Direction.East);
  }
}
実行結果
dの方角は North です。
dの持つ値は 0 です。
Direction.North の持つ値は 0 です。
Direction.West の持つ値は 1 です。
Direction.South の持つ値は 2 です。
Direction.East の持つ値は 3 です。
Press any key to continue

このように、列挙体の値の名前を文字列として表記することも可能ですし、値が内部的に保持している数値として表記することも可能です。 このように、列挙体の値が数値を持つだけでなく、意味も同時に持つことができる点で単なる定数より優れているといえます。



§2 列挙体の値と型

§2.1 列挙体定数の値

列挙体の定数一つ一つを宣言する際に、値を指定することができます。 なにも指定しない場合は、上から順に0, 1, 2...となっていきます。 その定数に特別な数値を与える場合、数値が意味を持つ場合には数値を指定することができます。

列挙体定数に値を与える
using System;

// 上下方向を表す列挙体
enum UpAndDown {
  Up   =  1,
  Down = -1,
  None =  0,
}

class Sample {
  static void Main()
  {
    int locationY = 5;
    UpAndDown upDown = UpAndDown.Down;

    Console.WriteLine("upDown の方向は {0} です。", upDown);

    locationY += (int)upDown;

    Console.WriteLine("locationY の位置は {0} です。", locationY);
  }
}
実行結果
upDown の方角は Down です。
locationY の位置は 4 です。
Press any key to continue

この例では上下方向を表す列挙体UpAndDownを宣言し、その各定数に値を与えています。 そして、upDownが上下(または方向なし)のうちどこを向いているかによってY座標の位置をずらしています。 このように、列挙体定数の値を直接利用することができます。
値を与える場合、複数の列挙体定数に同じ値を設定することができますが、その場合は次の例で示すような現象が発生します。

列挙体定数に同じ値を指定する
using System;

// 同じ値を持つ列挙体
enum SameValueEnum {
  ValueA = 0,
  ValueB = 1,
  ValueC = ValueA,
}

class Sample {
  static void Main()
  {
    SameValueEnum e = SameValueEnum.ValueB;

    Console.WriteLine("e: {0}", e);

    e = SameValueEnum.ValueC;

    Console.WriteLine("e: {0}", e);
  }
}
実行結果
e: ValueB
e: ValueA
Press any key to continue

この例ではSameValueEnumのValueCにValueAと同じ値を指定しています。 実行結果を見てわかるとおり、一度目の表示では正しく ValueBが表示されていますが、二度目の表示ではeにValueCを割り当てたはずなのにValueAと出力されてしまっています。 これは列挙体の定数は内部では結局数値として扱われてしまうのでこのようになるのです。 ValueAとValueCに相当する定数が互いに別名であるような場合には問題ありませんが、この例のようにValueCと出力されることを期待する場合には注意が必要です。

§2.2 列挙体定数の型

先の例のように列挙体定数の値を直接利用するようになると、列挙体定数の値の型が気になってきます。 そのような場合、列挙体の値の型を明示的に指定することができます。 指定できる型は、列挙体の性質からchar型以外の整数型に限られます。 また、なにも指定しなかった場合は既定でint(Integer)になります。 次の例は列挙体定数の型をshortにした例です。

列挙体の型を指定する
using System;

// 上下方向を表す列挙体
enum UpAndDown : short {
  Up   =  1,
  Down = -1,
  None =  0,
}

class Sample {
  static void Main()
  {
    short locationY = 5;
    UpAndDown upDown = UpAndDown.Down;

    Console.WriteLine("upDown の方向は {0} です。", upDown);

    locationY += (short)upDown;

    Console.WriteLine("locationY の位置は {0} です。", locationY);
  }
}

§3 列挙体に対する操作

列挙体は単純な定数に比べ様々な機能を持っています。 また、列挙体に対する操作もいろいろあります。 列挙体の暗黙の基底クラスであるEnum型には様々な静的メソッドが存在します。

§3.1 値の名前の取得 (GetName)

GetNameメソッドを使用すると、指定された値から該当する数値をもつ列挙体の定数の名前を取得することが出来ます。

列挙体定数の名前の取得
using System;

// 方角を表す列挙体
enum Direction : int {
  North = -1,
  West  = -2,
  South =  1,
  East  =  2,
  None  =  0,
}

class Sample {
  static void Main()
  {
    string name;

    // --3 から 3 までの値が示すDirectionの名前を表示する
    for (int i = -3; i <= 3; i++) {
      // iの値と同じ値を持つ列挙体定数の名前を取得
      name = Enum.GetName(typeof(Direction), i);

      Console.WriteLine("{0} means {1}", i, name);
    }
  }
}
実行結果
-3 means 
-2 means West
-1 means North
0 means None
1 means South
2 means East
3 means 
Press any key to continue

このように、指定された数値に対応する定数の名前を取得することができます。 また、対応する定数がない場合はnullが返されます。

§3.2 すべての名前・値の取得 (GetNames, GetValues)

単一の値から名前を取得するのではなく、列挙体に含まれるすべての定数の名前を取得するにはGetNamesメソッドメソッドを、すべての定数の値を取得するには GetValuesメソッドを使用します。 この例を次に示します。

列挙体に含まれるすべての名前、値を取得する
using System;

// 方角を表す列挙体
enum Direction : int {
  North = -1,
  West  = -2,
  South =  1,
  East  =  2,
  None  =  0,
}

class Sample {
  static void Main()
  {
    // Directionの持つ名前
    string[] names = Enum.GetNames(typeof(Direction));

    Console.WriteLine("'Direction' has following names");

    foreach (string name in names) {
      Console.WriteLine("  {0}", name);
    }

    // Directionの持つ値
    Array values = Enum.GetValues(typeof(Direction));

    Console.WriteLine("'Direction' has following values");

    foreach (int val in values) {
      Console.WriteLine("  {0}", val);
    }
  }
}
実行結果
'Direction' has following names
  None
  South
  East
  West
  North
'Direction' has following values
  0
  1
  2
  -2
  -1
Press any key to continue

Enum.GetValues()メソッドが実際に返す配列の型は、列挙体の値の型と同じものになります。 int(Integer)型の列挙体の場合は、int(Integer)型の配列が返されます。

§3.3 値が定数として宣言されているか (IsDefined)

任意の値が列挙体定数として宣言されているか否かを知るには、IsDefinedメソッドを使用します。

列挙体定数として宣言されているかを知る
using System;

// 方角を表す列挙体
enum Direction : int {
  North = -1,
  West  = -2,
  South =  1,
  East  =  2,
  None  =  0,
}

class Sample {
  static void Main()
  {
    Type t = typeof(Direction);

    for (int i = -3; i <= 3; i++) {
      // その値は定数として宣言されているか
      if (Enum.IsDefined(t, i)) {
          Console.WriteLine("{0} is defined in {1} as {2}.", i, t.Name, Enum.GetName(t, i));
      }
      else {
          Console.WriteLine("{0} is not defined in {1}.", i, t.Name);
      }
    }
  }
}
実行結果
-3 is not defined in Direction.
-2 is defined in Direction as West.
-1 is defined in Direction as North.
0 is defined in Direction as None.
1 is defined in Direction as South.
2 is defined in Direction as East.
3 is not defined in Direction.
Press any key to continue

§3.4 文字列からの変換 (Parse)

文字列(もしくは文字列化された数値)から列挙体の値を取得するには、Parseメソッドを使用します。 このメソッドでは、大文字・小文字の違いを無視するかどうかを指定することも出来ます。

文字列から列挙体定数の値を取得する
using System;

// 方角を表す列挙体
enum Direction : int {
  North = -1,
  West  = -2,
  South =  1,
  East  =  2,
  None  =  0,
}

class Sample {
  static void Main()
  {
    Type t = typeof(Direction);
    Direction d;

    // 大文字小文字を区別して"South"と名付けられた値を取得する
    d = (Direction)Enum.Parse(t, "South", false);

    Console.WriteLine(d);

    // 大文字小文字を無視して"north"と名付けられた値を取得する
    d = (Direction)Enum.Parse(t, "north", true);

    Console.WriteLine(d);

    // 指定された値から列挙体の値を取得する
    d = (Direction)Enum.Parse(t, "-2", true);

    Console.WriteLine(d);

    // 指定された値が見つからない場合はArgumentExceptionがスローされる
    try {
      Enum.Parse(t, "南", true);
    }
    catch (ArgumentException ex) {
      Console.WriteLine(ex.Message);
    }
  }
}
実行結果
South
North
West
要求された値 南 が見つかりませんでした。
Press any key to continue

§3.5 文字列への変換 (ToString)

列挙体の値を文字列化するには、、ToStringメソッドを使用します。 引数で文字列化する際の書式を指定することも出来ます。

列挙体定数の値を文字列化する
using System;

// 方角を表す列挙体
enum Direction : int {
  North = -1,
  West  = -2,
  South =  1,
  East  =  2,
  None  =  0,
}

class Sample {
  static void Main()
  {
    for (int i = -3; i <= 3; i++) {
      Direction d = (Direction)i;

      // 列挙体の値を文字列化する
      Console.WriteLine("{0,2}: {1,-8} {2,-8} {3,-8} {4,-8}", 
                        i, d.ToString(), d.ToString("G"), d.ToString("D"), d.ToString("X"));
    }
  }
}
実行結果
-3: -3       -3       -3       FFFFFFFD
-2: West     West     -2       FFFFFFFE
-1: North    North    -1       FFFFFFFF
 0: None     None     0        00000000
 1: South    South    1        00000001
 2: East     East     2        00000002
 3: 3        3        3        00000003

列挙体に対して指定できる書式文字列については書式指定子 §.列挙型の書式指定子で詳しく解説しています。

§3.6 値が特定のフラグを持つか (HasFlags)

.NET Framework 4より、FlagsAttributeの付いた列挙体の値が特定のフラグを持つかどうか調べるためのメソッドHasFlagが追加されています。 FlagsAttributeについては、列挙体とフラグで詳しく解説します。

HasFlagメソッドの説明によると、このメソッドは次のビット演算と同じ結果を返すとされています。

The HasFlag method returns the result of the following Boolean expression.

thisInstance And flag = flag

Enum.HasFlag Method (Enum)

HasFlagsメソッドとビット演算を使った場合の記述を比較すると次のようになります。 上記のとおり、以下のどちらも同等の処理となります。

HasFlagsメソッドとビット演算によるフラグチェックの比較
using System;
using System.IO;

class Sample {
  static void Main()
  {
    FileAttributes attrs = FileAttributes.Archive | FileAttributes.ReadOnly;

    if (attrs.HasFlag(FileAttributes.Archive))
      Console.WriteLine("archive");

    if ((attrs & FileAttributes.Archive) == FileAttributes.Archive)
      Console.WriteLine("archive");
  }
}

HasFlagメソッドを使うとコードは見やすくなる一方で、ビット演算を使った場合と比べてパフォーマンスは劣化します。 そのため、速度が優先される場合や繰り返しフラグの比較を行う状況ではあまり使いやすいものではありません。 参考までに、HasFlagsメソッドとビット演算での速度を比較した結果を示します。

.NET Frameworkで実行した場合
Microsoft Windows NT 6.0.6002 Service Pack 2
4.0.30319.1

Enum.HasFlag: 00:00:14.3554873
bitwise     : 00:00:00.0125018
Mono 2.8.1 master/0440378で実行した場合
Unix 2.6.32.25
4.0.30319.1

Enum.HasFlag: 00:00:08.2939831
bitwise     : 00:00:00.0170639
検証に使ったコード
using System;
using System.Diagnostics;
using System.IO;

class Test {
  static void Main()
  {
    Console.WriteLine(Environment.OSVersion);
    Console.WriteLine(Environment.Version);
    Console.WriteLine();

    const int act = 10 * 1000 * 1000;
    var attrs = FileAttributes.Archive | FileAttributes.ReadOnly;

    var sw1 = Stopwatch.StartNew();

    for (var i = 0; i < act; i++) {
      var isArchive = attrs.HasFlag(FileAttributes.Archive);
      var isSystem  = attrs.HasFlag(FileAttributes.System);
    }

    sw1.Stop();

    Console.WriteLine("Enum.HasFlag: {0}", sw1.Elapsed);

    var sw2 = Stopwatch.StartNew();

    for (var i = 0; i < act; i++) {
      var isArchive = (attrs & FileAttributes.Archive) == FileAttributes.Archive;
      var isSystem  = (attrs & FileAttributes.System)  == FileAttributes.System;
    }

    sw2.Stop();

    Console.WriteLine("bitwise     : {0}", sw2.Elapsed);
  }
}