Logo wizaman's blog (legacy)

C#のenumの使い方

August 14, 2016
11 min read
Table of Contents

仕事が忙しすぎて書きたかったネタが全然まとまらずに、技術関連の文章がまとまっていくアレ。

C#でのenumの使い方を簡単にまとめ。

私はWPFよりかは基本的にUnityでC#使ってます。Debug.Log()はUnityでのコンソール出力です。

public enum Type
{
    Invalid,
    A,
    B,
    C,
}

特に断りなければ、こんな定義のenum使ってると思ってください。

**C#のenumは内部的には整数型です。**値の割り当てを省略すれば、先頭では0が割り当てられ、以降は直前の定義に+1した値となります。C/C++でもおなじみのよくあるルールです。Javaはすっこんでいてください。

公式ドキュメントはこのあたり。言語機能そのものの説明はあまりしないので、詳しく知りたかったら自分で調べてねという感じで。

デフォルト値

enum型変数の初期値を明示的に書かなかったとき、デフォルト値が使われます。デフォルト値はdefaultキーワードを使って下記のように取得することもできます。defaultキーワードはenumじゃなくても使えるので覚えておくといいでしょう。

Type type = default(Type);

enum型の定義によらず、デフォルト値は「0」となります。「0」に対応する値を定義していなくても問答無用で「0」になります。

定義していない値が入り込む余地を作るのは運用上のトラブルを招きかねないため、普通はenum型には「0」に相当する値を定義しておきます。デフォルトで利用してほしい値や、無効値としての値を「0」に割り当てておくのがいいでしょう。Type.Invalidを最初に定義しているのはそのためです。

型変換

整数⇔enum⇔文字列の相互変換が可能です。また、Enumクラスによってenum関連の各種操作が提供されます。

// enum→整数
var x = (int)Type.A;
 
// 整数→enum
var type = (Type)0;
 
// enum→文字列
var str = Type.A.ToString();
var str = (string)Type.A;
 
// 文字列→enum
var type = (Type)Enum.Parse(typeof(Type), "A");

整数からenumへの変換は、たとえそれが定義されていない値であってもエラーとならず不正な値が入り込む余地があるため注意が必要です。Enum.IsDefined()で値の存在チェックができます。

一方、Enum.Parse()はパースできない文字列を与えるとエラーを吐くため、こちらはEnum.IsDefined()で先に存在チェックしておくといいでしょう。Enum.IsDefined()は、整数値も名前も定義チェックできるわけです。

Enum.IsDefined()はリフレクションを使った汎用処理のようで、どうしても処理速度が気になる場合は、地道にswitch-case文を書いたほうが早いみたいです。地道な手法はミスを招くので勧めませんが。名前指定であってもswitch-case文のほうが有意差があるほど早いのかは調べてませんが、case文に書く名前を多分間違えるので、計測するまでもなく名前チェックは素直にEnum.IsDefined()でいいと思います。

Enum.TryParse()が使えたらよかったですが、Unityで利用するC#のバージョンには含まれないようです。

(2016/09/04 追記)
書き忘れてましたが、enumのToString()もリフレクション経由のため遅い処理です。

型指定

内部表現に使う整数型はデフォルトでintですが、char以外の整数型を指定することもできます。公式ドキュメントからサンプル引用。

enum Days : byte {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};

ビットフラグ

FlagsAttribute属性をつけるとビットフラグとしての振る舞いができるようになります。インスタンスメソッドとして提供されるHasFlagでフラグを持つか調べられますが、UnityのC#バージョンでは利用できないので、普通に論理積で調べましょう。

反復操作

Enumクラスを利用して値や名前の列挙ができます。

// 名前の列挙
foreach(var name in Enum.GetNames(typeof(Type))) {
    Debug.Log(name);
}
 
// 値の列挙
foreach(Type type in Enum.GetValues(typeof(Type))) {
    Debug.Log(typ);
}

これらは配列として取得されるため、それぞれ個数を参照することもできます。

型制約

enumをジェネリックの型制約で指定できたらすごく嬉しいと常々思っているのですが、結論から言うとできません。

enum型を定義すると、暗黙的にEnumクラスを継承しているかのように振る舞い、各種メソッドが使えたりしますが、Enumクラスを型制約に指定することができません。

enum型はあくまで値型(struct)なのですが、EnumクラスはValueTypeクラスを継承した参照型(class)です。矛盾しているようですが、すべての値型と参照型もobjectクラスを継承している扱いになっているのと同じようなことです。このようなクラスは特殊クラスと呼ばれ、型制約に指定できません。

コンパイル時に制約を与えるには、値型であることを示すstructキーワードと、オブジェクトの変換可能なことを示すインターフェースIConvertibleで型制約を与えて、整数値への変換を許可するぐらいが限度ですかね。あとは実行時に型情報を調べて、enumかどうかをチェックして適宜エラー対応するしかないです。ちょい不便。

public class Sample<T>
    where T : struct, IConvertible
{
    public Sample()
    {
        var type = typeof(T);
        Debug.Assert(type.IsEnum, type.Name + " is not enum.");
    }
}

Unityのバージョンが古いとDebug.Assert()が使えないかもしれませんが、どのバージョンで導入されたのか知りません。

拡張メソッド

C#の強力な言語機能である拡張メソッドが、enumにも当然使えます。

例えば、RPGなどでよくある属性をenumで定義して、その日本語名や弱点属性を拡張メソッドから引っ張るとかできます。

public enum Element
{
    Normal,
    Fire,
    Water,
    Electricity,
    Wood,
}
 
public static class ElementExtension
{
    public static string getName(this Element element)
    {
        switch(element) {
        case Element.Normal: return "無";
        case Element.Fire: return "炎";
        case Element.Water: return "水";
        case Element.Electricity: return "電気";
        case Element.Wood: return "木";
        default:
            Debug.LogError("Unexpected Element: " + element);
            break;
        }
        return "";
    }
    public static Element? getWeakness(this Element element)
    {
        switch(element) {
        case Element.Normal: return null;
        case Element.Fire: return Element.Water;
        case Element.Water: return Element.Electricity;
        case Element.Electricity: return Element.Wood;
        case Element.Wood: return Element.Fire;
        default:
            Debug.LogError("Unexpected Element: " + element);
            break;
        }
        return null;
    }
}

Type?と書いているのは、null許容型です。

属性

各enum値にそれぞれ属性(Attribute)をつけることもできるため、属性として付加データを持たせておき、これを拡張メソッドで参照できるようにする、という使い方もできます。

public class NameAttribute : Attribute
{
    public NameAttribute(string name)
    {
        this.name = name;
    }
 
    public string name = "";
}
 
public enum Type
{
    [Name("無")]
    Normal,
    [Name("炎")]
    Fire,
    [Name("水")]
    Water,
    [Name("電気")]
    Electricity,
    [Name("木")]
    Wood,
}
 
public static class TypeExtension
{
    public static string getName(this Type type)
    {
        var fieldInfo = type.GetType().GetField(type.ToString());
        var attributes = (NameAttribute[])fieldInfo.GetCustomAttributes(typeof(NameAttribute), false);
        if(attributes.Length > 0) {
            return attributes[0].name;
        }
        return "";
    }
}

こっちの方がswitch文なんて使ってないし、enum定義のところに名前など関係するデータをまとめて書けるので、さっきの拡張メソッドの例よりも見た目がすっきりして扱いやすいですね。定義がまとまっていればcase文の追加し忘れのような運用ミスは防ぎやすいでしょう。

但し、リフレクションを使っているために、switch文よりもずっと遅くなります。頻繁に呼ぶところでは避けたいですね。

辞書のキーに利用

enumは内部表現が整数ですが、辞書クラス(Dictionaryなど)のキーに利用したとき、整数として処理しないためにパフォーマンスが落ちます。膨大な量を処理するために、効率のいいenum辞書が欲しければ、整数をキーにして適宜キャストしてやりましょう。