そもそも enum って何なのよ
というわけで、改めて enum をちょっと振り返ってみた。
まあ、自分的にはコレといって新しい発見は無かったのだが、せっかく改めて振り返ったので、ここに .NET の enum に関して、ちょっとメモ程度に残しておこう。
まず、.NET の enum は Java 等のソレとは異なり、まっとうなオブジェクトとして成立していない、どっちかというと C/C++ のソレと似たような性質を持つ。このあたりについては、ECMA に提出された CLI という文書のパーティション II に記載されているが、要約すると、
という点ぐらいで、C# ちっくに記述してみると、
enum には、対応する整数型を指定しなければならない
public enum MyEnum1 // 省略時は int である { A, B, C, D, } public enum MyEnum2 : short { A = 0, B, C, D, E = 0, }
この型は、enum のコンパイル結果に大きく依存する。上記のような enum は実質的に、次のようにコンパイルされる。
public struct MyEnum1 : System.Enum { public int value; public static readonly MyEnum1 A = 0; public static readonly MyEnum1 B = 1; public static readonly MyEnum1 C = 2; public static readonly MyEnum1 D = 3; // ここで MyEnum1 に整数である 0 や 1 が代入できる理由はすぐ後に }
構造体は親クラスを指定することができず、常に System.ValueType の派生クラスとなるのだが、enum の場合は特別に ValueType を派生した Enum の派生クラスとなる。
このような型定義がコンパイル結果に含まれることからして、enum はきちんとしたオブジェクトとして認識されているんじゃないか? と思われるかもしれないが、この型定義が利用されるのは、型情報を直接操作するときと、ValueType 特有のボックス化が発生するときぐらいである。
enum 型の変数は対応する整数型として扱われる
以下のようなコードは、
public void Method1(MyEnum1 e1, MyEnum2 e2) { if ((int) e1 == (int) e2) { Console.WriteLine("Equal!"); } }
次のような感じのコードとしてコンパイルされるわけである。
public void Method1([EnumType(typeof(MyEnum1))] int e1, [EnumType(typeof(MyEnum2))] short e2) { if (e1 == e2) { Console.WriteLine("Equal!"); } }
当然、標準クラスライブラリのどこを探しても EnumTypeAttribute などというクラスはない。あくまでイメージの話である。
このような形にコンパイルされる結果、MyEnum1 や MyEnum2 というのは、メソッドのインターフェスに現れ、e1 や e2 といったローカル変数を修飾するメタ情報としてしか成り立っていない。最初の enum を展開した struct の各 static field が、MyEnum1 型であるにもかかわらず整数型を代入しているのは、このような仕組みの都合で、実際は MyEnum1 型のフィールドは int 型のフィールドであり、代入が可能であるからということになる。
次のようなコードの場合、
public void Method2() { MyEnum1 e = MyEnum1.B; Console.WriteLine( e.ToString() ); }
最初の行は、当然 int として、
public void Method2() { [EnumType(typeof(MyEnum1))] int e = 1; Console.WriteLine( e.ToString() ); }
というイメージに展開される。しかし、次の Console.WriteLine() の引数である e.ToString() をそのまま実行してしまうと、"1" という文字列が出力されることになるが、実際に上記のコードを実行すると "B" という文字列が出力される。
つまり、仮称 EnumTypeAttribute にて修飾された整数型に対して、メソッド呼び出しが発生する時には、いくつかの魔法掛かった手法が実行されるのである。
まあ、魔法掛かったといっても、通常 int (System.Int32) のような型に対してメソッド呼び出しが発生する場合、参照型の this を生成するためボックス化が発生し、IL は、
ldarg e // e を box System.Int32 // Int32 型としてボックス化し callvirt System.Object::ToString() // ソイツに対して ToString() を呼ぶ
のような感じのコードになるのだが、この際のボックスオペコードのオペランドが System.Int32 にならずに、仮称 EnumTypeAttribute の引数である MyEnum1 型が利用されることで、
ldarg e // e を box MyEnum1 // MyEnum1 型としてボックス化し callvirt System.Object::ToString() // ソイツに対して ToString() を呼ぶ
というような IL になる*1、というだけのことだったりする。簡単に見ると、enum 型は自身と互換性のある整数型のボックス化先として適正なつくりになっているということだ。
それで、Enum.ToString() はなぜ遅いのか
上記の事柄とはまったく無縁に、Enum.ToString() は遅い。
このメソッドにステップインしてトレースしたりすると、ものすごい量のコードの海に突撃してしまう。
いったい何をやってるんだとびっくりするぐらい複雑な処理をしている。
何をやってるか知りたい人は、Microsoft .NET Framework の実装を ildasm や類似のツールで読むなり、SSCLI のソースコードを読むなりしてみればわかるかもしれないが、FlagsAttribute をサポートする程度では考えられないぐらいのコード量がぎっしり詰まっている。
内容的に、リフレクションで static readonly な各フィールドの名前を毎回ひっぱると重たいので、ちょっとキャッシュして高速化してみようか、ってかんじになっているのだが、手元のプロジェクトだと、このキャッシュがまったく効いてなくて、毎回毎回リフレクション経由になる上に、リフレクション経由の場合には、次回のために(ヒットしない)キャッシュを生成するから、さらに重たいという多重苦状態になっている。
*1:実際の所、Int32 も MyEnum1 も sealed な型なので、callvirt ではなく、型情報から導かれる具体的なメソッドを直接呼び出す call オペコードが生成される。