読者です 読者をやめる 読者になる 読者になる

C#でタイプセーフEnumパターン

C#でのテストはちょっとお休みして、今日はC#でタイプセーフEnumパターンの実現方法をいくつか紹介する。


C#には十分にタイプセーフなenumがあるが、それでも基本は数値型であり、switchやifによる分岐が必要になってしまう点はC言語C++enumと何ら変わらない。
ということで、C#でタイプセーフEnumパターンを実装することには意味がある。もちろん、C#本来のenumで十分な場合も多いので、使うか使わないかはの判断は必要だ。


まずは、C#enumに対して全く利点のない単純なものから。

public sealed class Enum1
{
    private Enum1() {}
    
    public static readonly Enum1 Value1 = new Enum1();
    public static readonly Enum1 Value2 = new Enum1();
}

これは単に、継承を禁止してコンストラクタをprivateにしただけのものだが、出発点としては十分な例でもある。


次は、C#enumの欠点の一つである、文字列化が面倒という点を解消するのに十分なものだ。

public sealed class Enum2
{
    public readonly string Str;
    private Enum2(string str) { Str = str; }
    
    public override string ToString() { return Str; }
    
    public static readonly Enum2 Value1 = new Enum2("value 1");
    public static readonly Enum2 Value2 = new Enum2("value 2");
}

基本的には上の例にpublic readonlyなフィールドを追加し、ToStringをオーバーライドしただけである。
Strはprivateにしたり、読み取り専用のプロパティにしたりと、いくつかの変形が考えられる。


次は、要素間に順序を追加するもので、これだけならC#enum劣化コピーでしかない*1が、ほかのものと組み合わせると便利だ。

public sealed class Enum3
{
    private static int nextOrdinal = 0;
    public readonly int Ordinal = nextOrdinal++;
    private Enum3() {}
    
    public static readonly Enum3 Value1 = new Enum3();
    public static readonly Enum3 Value2 = new Enum3();
}

ちなみに、この例はEffective Javaの「順序に基づくタイプセーフenum」を参考にしている。単純化するためにIComparableは実装していない。


次は、数値からタイプセーフEnumを取り出せるような拡張だ。

public sealed class Enum4
{
    private static int nextId = 0;
    private static Dictionary<int, Enum4> values = new Dictionary<int, Enum4>();
    private Enum4() { values.Add(nextId++, this); }
    
    public static Enum4 ValueOf(int id)
    {
        if (values.ContainsKey(id))
            return values[id];
        throw new ArgumentOutOfRangeException();
    }
    
    public static readonly Enum4 Value1 = new Enum4();
    public static readonly Enum4 Value2 = new Enum4();
}

見つからなかった場合はnullを返したり、Null Objectを返す*2といった変形が考えられる。
この例ではDictionaryである必要はない*3が、idが連番ではない場合等にはDictionaryが必要となる。


次は、タイプセーフEnumに振る舞いを持たせる拡張*4だ。

public sealed class Enum5
{
    private delegate void ProcDelegate();
    private ProcDelegate proc;
    private Enum5(ProcDelegate proc) { this.proc = proc; }
    
    public void Proc() { proc(); }
    
    public static readonly Enum5 Value1 =
        new Enum5(delegate { Console.WriteLine("hoge"); });
    public static readonly Enum5 Value2 =
        new Enum5(delegate { Console.WriteLine("piyo"); });
}
void Proc(Enum5 e)
{
    e.Proc();
}

Javaではanonymous classで実現しているところを、C#版では匿名メソッドを使用している。
さらにこれを発展させ、呼び出し元で処理を決定できるようにすることも可能である*5

public sealed class Enum6
{
    public delegate void ProcDelegate();
    
    private delegate void DispatchDelegate(ProcDelegate proc1, ProcDelegate proc2);
    private DispatchDelegate dispatcher;
    private Enum6(DispatchDelegate dispatcher) { this.dispatcher = dispatcher; }
    
    public void Proc(ProcDelegate proc1, ProcDelegate proc2)
    {
        dispatcher(proc1, proc2);
    }
    
    public static readonly Enum6 Value1 =
        new Enum6(delegate(ProcDelegate proc1, ProcDelegate proc2) { proc1(); });
    public static readonly Enum6 Value2 =
        new Enum6(delegate(ProcDelegate proc1, ProcDelegate proc2) { proc2(); });
}
void Proc(Enum6 e)
{
    e.Proc(
        delegate { Console.WriteLine("value 1"); },
        delegate { Console.WriteLine("value 2"); }
    );
}


最後は、グルーピング。

public sealed class Enum7
{
    private Enum7() {}
    
    public static class Inner1
    {
        public static readonly Enum7 Value1 = Enum7();
        public static readonly Enum7 Value2 = Enum7();
    }
    
    public static class Inner2
    {
        public static readonly Enum7 Value1 = Enum7();
        public static readonly Enum7 Value2 = Enum7();
    }
}

privateなコンストラクタをインナークラスから呼び出しているのがポイントだ。


以上、タイプセーフEnumパターンのみ7つでした。

*1:キャストが不要ということを考えると単純に劣化とも言えない・・・?

*2:この例ではnullを返すのと変わらないが、ほかのパターンと組み合わせると例外を投げるより使いやすくなる

*3:Listでいい

*4:これもEffective Javaを参考にしている

*5:これはJavaプログラミングの処方箋の「タイプセーフなenumの値による分岐」を参考にした