列挙型

列挙型とは

列挙型は、複数の数値に名前を付けてまとめることができる機能です。
定数を一度に定義する、と考えることもできます。

例えば引数の値で処理を分岐するメソッドを作ってみます。


static void ShowDayOfWeek(int dayOfWeek)
{
    switch(dayOfWeek)
    {
        case 0:
            Console.WriteLine("今日は日曜日です");
            break;
        case 1:
            Console.WriteLine("今日は月曜日です");
            break;
        case 2:
            Console.WriteLine("今日は火曜日です");
            break;
        case 3:
            Console.WriteLine("今日は水曜日です");
            break;
        case 4:
            Console.WriteLine("今日は木曜日です");
            break;
        case 5:
            Console.WriteLine("今日は金曜日です");
            break;
        case 6:
            Console.WriteLine("今日は土曜日です");
            break;
        default:
            Console.WriteLine("曜日が不明です");
            break;
    }
}

static void Main(string[] args)
{
    ShowDayOfWeek(0);
    ShowDayOfWeek(3);

    Console.ReadLine();
}
今日は日曜日です
今日は水曜日です

ShowDayOfWeekメソッドは、引数の数値に応じて曜日を表示するメソッドです。
0なら日曜日、1なら月曜日…と処理を振り分けます。

これでも一応機能はします。
しかし「0」が日曜日、「1」が月曜日…というのはこのコードを書いたプログラマが勝手に決めた法則で、他人が読んだときに非常にわかりづらいものとなっています。
このようなコード中に現れる意味のわかりづらい数値はマジックナンバーといい、避けるべきとされています。

仮に自分しか使わないような個人的なコードでも、日が経つとこういった「約束事」は結構忘れてしまうものです。
コメントに残すという手もありますが、このような場合には列挙型を使用すべきです。

上記コードを列挙型を用いて書き直したのが以下のコードです。


enum DayOfWeek
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

static void ShowDayOfWeek(DayOfWeek dayOfWeek)
{
    switch(dayOfWeek)
    {
        case DayOfWeek.Sunday:
            Console.WriteLine("今日は日曜日です");
            break;
        case DayOfWeek.Monday:
            Console.WriteLine("今日は月曜日です");
            break;
        case DayOfWeek.Tuesday:
            Console.WriteLine("今日は火曜日です");
            break;
        case DayOfWeek.Wednesday:
            Console.WriteLine("今日は水曜日です");
            break;
        case DayOfWeek.Thursday:
            Console.WriteLine("今日は木曜日です");
            break;
        case DayOfWeek.Friday:
            Console.WriteLine("今日は金曜日です");
            break;
        case DayOfWeek.Saturday:
            Console.WriteLine("今日は土曜日です");
            break;
        default:
            Console.WriteLine("曜日が不明です");
            break;
    }
}

static void Main(string[] args)
{
    ShowDayOfWeek(DayOfWeek.Sunday);
    ShowDayOfWeek(DayOfWeek.Wednesday);

    Console.ReadLine();
}
今日は日曜日です
今日は水曜日です

メソッドの呼び出し側では、日曜日の処理を呼び出すために引数にDayOfWeek.Sundayという値を指定しています。
名前がついているので、数値の「0」を渡すよりも意味が分かりやすくなっています。

列挙型の定義は以下の形式で行います。


enum 列挙型の名前
{
    要素1,
    要素2,
    要素3,
    ...
}

enumというのが列挙型を意味するキーワードです。
記述する場所はメソッドの定義の時と同じく、Mainメソッドの隣です。

enumに続き、列挙型の名前を記述します。
名前は変数名やメソッド名の時と同じく自由です。
先頭に数値(0~9)を使用できないというのも同じです。

続くブロック内には定義したい名前をコンマ(,)で区切って記述していきます。
要素はいくつでも自由です。

ここで定義した列挙型は、Mainメソッド内などから列挙型名.要素名の形式で使用できます。
要素名はフィールドメンバともいいます。

列挙型変数

列挙型はユーザー定義型の一種で、新しいデータ型を定義することになります。
つまりその列挙型の変数を作ることができます。
上記のサンプルコードではメソッドの仮引数に列挙型を使用しています。

列挙型は何をしているのか?

上記コードを読んでも「いったい何をしているのか?」とピンとこないかもしれません。
列挙型は、最初に説明した通り「定数の定義」と考えるとわかりやすいです。

上記の列挙型の定義は実は省略した記述方法です。
省略せずに記述すると以下のようになります。


enum DayOfWeek
{
    Sunday      = 0,
    Monday      = 1,
    Tuesday     = 2,
    Wednesday   = 3,
    Thursday    = 4,
    Friday      = 5,
    Saturday    = 6
}

「Sunday」は「0」、「Monday」は「1」…と、0から順番に数値に名前を与えているのです。
コード中にDayOfWeek.Wednesdayと書けば、それは内部的には「3」と書いたことと同じになります。
ただし、普通の数値ではなく「DayOfWeek型」の値になるので、そのままでは足し算に使用したりはできません。
(数値に変換する方法は後述)

数値の指定を省略して記述すると、自動的に先頭から順に0、1、2…という数値が割り振られます。

フィールド値の定義

列挙型のフィールド値はかなり自由に設定できます。
例えば、


enum DayOfWeek
{
    Sunday = -2,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

とすれば、上から順に「-2、-1、0…」と自動的に割り振られます。


enum DayOfWeek
{
    Sunday,
    Monday,
    Tuesday = 5,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

とすれば、上から順に「0、1、5、6、7…」という値が割り振られます。
列挙型は、

  • 最初の要素の数値指定を省略すれば「0」、指定があればその数値
  • 次の数値はひとつ前の値に「1」加算する
  • 途中で数値指定があればその数値に変更
  • 以降も「1」ずつ加算

というルールで数値が割り振られます。


enum DayOfWeek
{
    Sunday,
    Monday,
    Tuesday     = -1,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

上記のような列挙型も作れますが、SundayWednesdayは同じ「0」になり、MondayWednesdayは同じ「1」になります。
列挙型に同じ値が含まれると名前でそれぞれの識別ができなくなるため、限定的な使用方法となるでしょう。

また、列挙型は少なくとも「0」となる値を含めておくことが推奨されます。
これは既定値が0であるためです。

列挙型内部のデータ型

列挙型は何も指定しなければ値の管理にはint型が使用されます。
データ型はbyte、sbyte、short、ushort、int、uint、long、ulong型のいずれかを使用できます。

例えばlong型に変更するには以下のように、列挙型名の後ろにコロン(:)を記述し、その後にデータ型を指定します。


enum DayOfWeek : long
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

通常の範疇ではint型のサイズで不足することはありませんが、ビットフィールドとして使用する場合は大きな値が必要になることもあります。

データ型の相互変換

以下の説明では特に指定のない限り、以下の列挙型を使用します。


enum DayOfWeek
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

名前や数値が定義されているかを調べる

指定の列挙型に、任意のフィールド名や数値が定義されているかをチェックするにはEnum.IsDefinedメソッドを使用します。


int n = 1;
bool b = Enum.IsDefined(typeof(DayOfWeek), n);
Console.WriteLine("DayOfWeek型に{0}は定義されていま{1}。",
    n,
    b ? "す" : "せん");

n = 10;
b = Enum.IsDefined(typeof(DayOfWeek), n);
Console.WriteLine("DayOfWeek型に{0}は定義されていま{1}。",
    n,
    b ? "す" : "せん");

string s = "Friday";
b = Enum.IsDefined(typeof(DayOfWeek), s);
Console.WriteLine("DayOfWeek型に{0}は定義されていま{1}。",
    s,
    b ? "す" : "せん");

s = "Everyday";
b = Enum.IsDefined(typeof(DayOfWeek), s);
Console.WriteLine("DayOfWeek型に{0}は定義されていま{1}。",
    s,
    b ? "す" : "せん");
DayOfWeek型に1は定義されています。
DayOfWeek型に10は定義されていません。
DayOfWeek型にFridayは定義されています。
DayOfWeek型にEverydayは定義されていません。

このメソッドの第一引数はtypeof(DayOfWeek)というものが指定されています。
typeofは指定のデータ型のTypeクラス型を取得する演算子です。

クラスについてはまだ詳しく説明していませんが、特定の機能を実現するために複数の変数や関数をひとまとめにしたものです。
Typeクラスは特定のデータ型の情報を保持しています。
ここではあまり深く考えずに、目的の列挙型の情報を取り出すための定型句としてそのまま覚えるだけで良いです。

第二引数には、チェックしたい数値または文字列を指定します。
文字列を指定した場合はフィールド名がチェックされます。
(大文字小文字は区別される)

戻り値は、指定の値が定義されていれば真、定義がなければ偽です。

ジェネリックメソッド版

.NET5以降Enum.IsDefinedメソッドは以下の書き方でも使用できます。


Console.WriteLine(
    Enum.IsDefined((DayOfWeek)1));

//型引数を省略しない書き方
Console.WriteLine(
    Enum.IsDefined<DayOfWeek>((DayOfWeek)10));
True
False

列挙型のデータ型を型引数という形でメソッドに渡しています。
これはジェネリックというC#の機能を利用した記法です。

型引数はメソッド名の直後の<>の中に指定します。
引数からデータ型が推測可能な場合はこれを省略できます。

引数は目的の列挙型です。
数値は列挙型にキャストして渡すことができますが、文字列はキャストできないのでジェネリックメソッド版で判定できるのは数値のみです。

数値に変換

列挙型の値を数値で得るには、列挙型をキャストします。


DayOfWeek dayOfWeek = DayOfWeek.Wednesday;

int numDayOfWeek = (int)dayOfWeek;

Console.WriteLine(numDayOfWeek);
3

以下のように直接数値に変換することもできます。


int numDayOfWeek = (int)DayOfWeek.Wednesday;

文字列に変換

列挙型で定義されているフィールド名の文字列を得るにはToStringメソッドを使用します。


DayOfWeek dayOfWeek = DayOfWeek.Wednesday;

string strDayOfWeek1 = dayOfWeek.ToString();

//直接文字列に変換する場合
string strDayOfWeek2 = DayOfWeek.Thursday.ToString();

Console.WriteLine(strDayOfWeek1);
Console.WriteLine(strDayOfWeek2);
Wednesday
Thursday

このメソッドは、列挙型のオブジェクト(変数)に続けて.ToString()を記述することで実行します。
この方法で呼び出すメソッドは.の手前のオブジェクトが持つ情報を利用して処理を行うもので、インスタンスメソッドと言います。
Console.WriteLineメソッドなどは静的メソッド(staticメソッド)と言います。
このあたりのことは標準ライブラリの項で改めて説明します。

数値を列挙型のフィールド名に変換

数値を列挙型で定義されているフィールド名に変換するには、列挙型にキャストしてからToStringメソッドを実行する方法があります。
このほかに、Enum.GetNameメソッドを使用することでも同等の処理が可能です。


string a = Enum.GetName(typeof(DayOfWeek), 1);
string b = Enum.GetName(typeof(DayOfWeek), 10);

Console.WriteLine(a);
Console.WriteLine(b); //null
Monday

戻り値はstring型でフィールド名を返します。
列挙型に定義されていない数値を指定するとnullが返されます。

列挙型に同じ数値で定義されているフィールドが複数存在する場合、どのフィールド名が返されるかは不定です。

数値を列挙型に変換

数値はキャストすることで列挙型に変換することができます。


DayOfWeek dayOfWeek = (DayOfWeek)3;

Console.WriteLine(dayOfWeek.ToString());
Wednesday

ただし列挙型で定義されていない値であってもキャストできてしまうので、その値が定義されていることが明らかでない場合は事前にEnum.IsDefinedメソッドでチェックする必要があります。


int n = 10;
//値に関係なく変換できてしまう
DayOfWeek dow1 = (DayOfWeek)10;
Console.WriteLine(dow1.ToString());

//変換可能かを事前チェック
if(Enum.IsDefined(typeof(DayOfWeek), n) ) {
    DayOfWeek dow2 = (DayOfWeek)n;
    Console.WriteLine(dow2.ToString());
}
else
{
    Console.WriteLine("{0}はDayOfWeek型に変換できません。", n);
}
10
10はDayOfWeek型に変換できません。

文字列を列挙型に変換

文字列はEnum.Parseメソッドで列挙型に変換することができます。


enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 4,
    Thursday = 8,
    Friday = 16,
    Saturday = 32
}

static void Main(string[] args)
{
    DayOfWeek dow1 = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), "Monday");
    DayOfWeek dow2 = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), "Tuesday, Wednesday");
    DayOfWeek dow3 = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), "thursday", true); //大文字小文字を無視

    Console.WriteLine(dow1);
    Console.WriteLine(dow2);
    Console.WriteLine(dow3);
}
Monday
6
Thursday

第一引数は列挙型をtypeofしたTypeクラスを指定します。
第二引数はその列挙型に定義されている文字列を指定します。
第三引数にtrueを指定すると、大文字小文字を無視することができます。
(デフォルトはfalse)

戻り値はobject型なので、目的の列挙型へのキャストが必要です。

第二引数の文字列は,で区切ることで複数同時に指定することができます。
(,の後に空白があっても良い)
このとき、列挙型の各値をOR演算した値が得られます。
これは主にビットフィールドを使用する場合に有効です。

このメソッドは列挙型に定義されていない文字列(フィールド名)を指定すると実行時エラー(例外)になります。
フィールド名が列挙型に定義されているか否かはEnum.IsDefinedメソッドで判定できます。

列挙型へ変換可能なら変換する

数値のキャストや、Enum.Parseメソッドによる文字列の列挙型への変換は、Enum.IsDefinedメソッドで目的の列挙型に定義があるかをチェックする必要があります。
Enum.TryParseメソッドは、定義のチェックと列挙型への変換を同時に行うことができます。
このメソッドは.NET Framework4以降で使用可能です。


DayOfWeek dow;
if(Enum.TryParse("Monday", out dow))
{
    Console.WriteLine(dow.ToString());
}

//数値もチェック可能
if (Enum.TryParse("5", out dow))
{
    Console.WriteLine(dow.ToString());
}
Monday
Friday

第一引数は変換する文字列です。
ここに数値を文字列として渡すことで数値の変換も可能です。

数値はToStringメソッドで文字列に変換することができます。


int n = 5;
DayOfWeek dow;

//int型変数を文字列に変換
if (Enum.TryParse(n.ToString(), out dow)) { }

//こんな書き方もできる
if (Enum.TryParse(5.ToString(), out dow)) { }

第二引数は列挙型変数をout参照渡しで指定します。

戻り値はbool型で、変換に成功した場合は真を、失敗した場合は偽を返します。
戻り値が真の場合、第二引数に変換後の列挙型の値が格納されます。

第二引数にtrueを指定すると、大文字小文字を無視したチェックが可能です。
列挙型変数の指定(out参照渡し)は第三引数にずれます。


DayOfWeek dow;
if(Enum.TryParse("sunday", out dow))
{
    Console.WriteLine("1: {0}", dow.ToString());
}

if (Enum.TryParse("sunday", true, out dow))
{
    Console.WriteLine("2: {0}", dow.ToString());
}
2: Sunday

このEnum.TryParseメソッドはジェネリックメソッドです。
第二引数(大文字小文字の判定を指定する場合は第三引数)の列挙型変数からデータ型が推測されます。
型引数を省略しないで記述すると以下のようになります。


DayOfWeek dow;
bool b = Enum.TryParse<DayOfWeek>("Sunday", out dow);

ジェネリックでない通常版のメソッドも存在しますが、変換結果を受け取る変数にobject型しか渡せず、使いづらいのであえて使用する必要はありません。


object dow; //object型変数
bool b = Enum.TryParse(typeof(DayOfWeek), "Monday", out dow);

列挙型の数値を一挙に得る

列挙型で定義されている数値をすべて得るにはEnum.GetValuesメソッドを使用します。


Array arrayDayOfWeeks = Enum.GetValues(typeof(DayOfWeek));

foreach(int n in arrayDayOfWeeks)
{
    Console.WriteLine(n);
}
0
1
2
3
4
5
6

引数はEnum.IsDefinedメソッドと同じく、列挙型にtypedef演算子を適用して取得したTypeクラスです。

戻り値はArrayというクラスのオブジェクトです。
これは配列と同じくデータの集合を扱うのですが、値はobject型で保存されています。
値を取り出すにはobject型から目的の型にキャストする必要があり、普通に使うには面倒なのであまり使われることがないクラスです。

Arrayクラスはforeach文でひとつずつ値を取り出せますが、繰り返し変数のデータ型にvar(型推論)を使用するとobject型で取得することになります。
取得後にキャスト演算子で型変換するのでも良いですが、最初に型推論ではなく目的のデータ型を指定すれば変換された型で繰り返し変数を使用できます。


foreach(var n in Enum.GetValues(typeof(DayOfWeek)))
{ 
    //変数nはobject型
    //毎回キャストが必要になる
    int num = (int)n;
}

foreach(int n in Enum.GetValues(typeof(DayOfWeek)))
{ 
    //変数nはint型
}

ジェネリックメソッド版

.NET5以降Enum.GetValuesメソッドはジェネリック版が使用できます。


Array arrayDayOfWeeks = Enum.GetValues<DayOfWeek>();

列挙型のフィールド名を一挙に得る

列挙型で定義されているフィールド名をすべて得るにはEnum.GetNamesメソッドを使用します。


string[] strDayOfWeeks = Enum.GetNames(typeof(DayOfWeek));

foreach(var s in strDayOfWeeks)
{
    Console.WriteLine(s);
}
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

使い方はEnum.GetValuesと同じです。
戻り値は単純なstring型配列なので、foreach文で型推論を使用しても問題ありません。

ジェネリックメソッド版

.NET5以降Enum.GetNamesメソッドはジェネリック版が使用できます。


string[] names = Enum.GetNames<DayOfWeek>();

列挙型の項目数の取得

C#には列挙型の項目数を得るメソッドは存在しないため、Enum.GetValuesEnum.GetNamesで項目を一括取得し、そのLengthプロパティで要素数を得ます。


//どちらも「7」
int count1 = Enum.GetValues(typeof(DayOfWeek)).Length;
int count2 = Enum.GetNames(typeof(DayOfWeek)).Length;

Enum.GetValuesのほうが戻り値のサイズが小さいことがほとんどだと思うので、こちらのほうが若干高速かもしれません。