β2 での anonymous type

以前からちょこちょこ書いてきたようにβ2で匿名型が「不変な型」に変わりました。いったん生成したらもう値は変更できません。どう変わったか、最小のコードを書いて、Release でのコンパイル結果を ILdasm で見てみました。

class Program
{
  static void Main()
  {
    var n = new { Name = "A", Age = 20 };
  }
}

まずは Main の IL から。

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       14 (0xe)
  .maxstack  8
  IL_0000:  ldstr      "A"
  IL_0005:  ldc.i4.s   20
  IL_0007:  newobj     instance void class '<>f__AnonymousType0`2'<string,int32>::.ctor(!0,
                                                                                        !1)
  IL_000c:  pop
  IL_000d:  ret
} // end of method Program::Main

「<>f__AnonymousType0`2」がこの型の仮名のようです。ジェネリックとして生成されてます。以前と違って string と int を引数に取るコンストラクタ一発で生成に変わったようです。

次に全体構成、ツリーのダンプ。

___[MOD] C:\Documents and Settings\Administrator\My Documents\Visual Studio 2008\Projects\AnonyTypeTest\AnonyTypeTest\bin\Release\AnonyTypeTest.exe
   |      M A N I F E S T
   |     <>f__AnonymousType0`2<'<Name>j__TPar','<Age>j__TPar'>
   |   |     .class private auto ansi sealed beforefieldinit 
   |   |     .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )  ...
   |   |     .custom instance void [mscorlib]System.Diagnostics.DebuggerDisplayAttribute::.ctor(string) = ( 01 00 1F 5C 7B 20 4E 61 6D 65 20 3D 20 7B 4E 61   // ...\{ Name = {Na ...
   |   |___[FLD] field <Age>i__Field : private initonly !1
   |   |___[FLD] field <Name>i__Field : private initonly !0
   |   |     method .ctor : void(!'<Name>j__TPar',!'<Age>j__TPar')
   |   |___[MET] method Equals : bool(object)
   |   |___[MET] method GetHashCode : int32()
   |   |___[MET] method ToString : string()
   |   |     method get_Age : !'<Age>j__TPar'()
   |   |     method get_Name : !'<Name>j__TPar'()
   |   |___[PTY] prop Age : instance !1()
   |   |___[PTY] prop Name : instance !0()
   |
   |___[CLS] Program
   |   |     .class private auto ansi beforefieldinit 
   |   |___[MET] method .ctor : void()
   |   |___[STM] method Main : void()
   |

ちょっと汚くて見づらいかもしれませんが、生情報ということで…。
「<>f__AnonymousType0`2<'j__TPar','j__TPar'>」の下にずらずらといろいろ生成されています。本文がたった1行のコードなのにこんなに! C#2.0以降、コード生成がお好きなようです。

詳しく見ていきます。まずは「.class private auto ansi sealed beforefieldinit」。private !?また無理やりな新しいルールを導入したようですね…。匿名型の型のスコープは一つのメソッドなので、これを private としたのでしょう。つまり、ほかのメソッドにある匿名型は見えないってことです。渡すこともできません。訂正:渡せます。渡せば見えます。ちょっとハンドリングが素直じゃないだけで。渡せないかも?よくわかりません…。
そして、sealed することで継承できなくしています。これは不変にするため。
フィールドには見慣れない initonly なんてのがついています。徹底して不変にしてます。
コンストラクタは Main で呼んでいたもの一つのみ。他の形式はなし。不変なのでこれは当然です。
プロパティは get だけで、set はありません。不変です。
ほかに3つのメソッドがあります。bool Equals( object )、GetHashCode、ToString()。なんと GetHashCode が自動的に生成されています!

Equals メソッドでは型チェック後、フィールドの値それぞれを System.Collections.Generic.EqualityComparer で比較しています。フツーです。
ここまで自動でやっといて IEquatable の bool Equals( T ) は生成しないんですね。ガッカリ…。と思ったけど、よく考えてみたら as が一つ入るかどうかの違いしかないのか。anonymous type って名前なのに class しか生成しないから、これは気にするほどのことではないのかも。
しかし、== 演算子と != 演算子のオーバーロードはしていません。Equals() と演算子 == のオーバーロードに関するガイドラインでは、不変な型の == 演算子のオーバーロードは有効とあるのに。代表的な不変な型の string と違うってのはどうなのよ?

これはつまり、

using System;

class Program
{
  static void Main()
  {
    var n  = new { Name = "A", Age = 20 };
    var n2 = new { Name = "A", Age = 20 };

    Console.WriteLine( n == n2 );
    Console.WriteLine( n.Equals( n2 ) );
    Console.ReadKey();
  }
}

の結果が False, True ということです。う〜ん。
ちなみに、この n と n2 は同じ型のプロパティを同じ順で持っているので、同じ型とみなしてくれます。

ToString はきちんと StringBuilder を使って、こんなのを作ります。

{ Name = A, Age = 20 }

で、最後に GetHashCode。どんな型にでも対応できるそれなりのハッシュ値を作ることができるの?ってのがよくわかりませんが、なんだかそれっぽい値を作り出すようです。こんな。

( 2097581262 * 2773833001 + Name.GetHashCode() ) * 2773833001 + Age.GetHashCode()

これが良いのか悪いのかさっぱりわかりませんが、匿名型だけ自動的に GetHashCode 書いてくれていいなぁ。2008の次のバージョンではほかの型向けにも同様のサポートが入るかもしれません。


ところで、ジェネリックとして生成されるのはコード爆発を抑えるためかと思ったけど、{ Name = "A", City = "B" }と{ City = "B", Name = "A" }としてもそれぞれ別の型としてコードが生成されました。== で比較しようとすると「型が違う」ってコンパイラに怒られました。そうか違うのか。Equals での比較も当然 False です。
ちなみに、{ Name = "A", City = "B", } のように最後の「,」はあってもOKでした。

var n4 = new { n.Name, n.Age };

こんなふうにプロパティ名を省略して書くと、自動でつけてくれたりもします。それぞれ、Name、Age の名前を勝手につけてくれます。つまり、この場合は n と同じ型になります。

気になるところは == のオーバーロードがないってところかな。