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

匿名型と型推論 (C# 3.0)

.NET

匿名型

C# 3.0 で導入される匿名型 (AnonymousType)*1 は,Tuple の要素にタグ (識別子) の付いたもの (いわゆるレコード) とみることができます.

var person = new {ID=1, Name="Alice"};
person = new {ID=2, Name="Bob"};

匿名型はコンパイラによってコンパイル時に生成される内部型ですが,同一の要素を同じ順序で並べた匿名型は,同じプログラム *2 の中で同じ型になることが保証されています.

匿名型で情報を返す

さて,匿名型は,文字通りその真の名前が隠されていて,フィールドやメソッドシグネチャに使いたくてもその型名を書くことができません.匿名型で情報を返したい場合はどうすればよいのでしょうか?
C# 2.0/C# 3.0 で導入される型推論を用いると,いくつかの場面で型名をあらわに書く必要がなくなります.むしろ,匿名型は真の型名を書くことができないことを考えれば,「型名を書かなくて良い」ことではじめて,匿名型を活用できるとも言えます.実際,先ほどのテストコードも,匿名型のオブジェクトは,キーワード var の変数で受ける以外には,object 型で受けることしかできません.
そこで,匿名型と同じくメソッドのブロック内に書け,かつ引数の型表記を省略できるラムダ式の活用を考えてみます.
準備として,Generic Method の型引数をコンパイラに推論させることで,ある匿名型を型引数に持つような Generic Type を作るところから見てみましょう.

static IEnumerable<T> ToEnumerable<T>(T arg)
{
    yield return arg;
}

var person = new {ID=1, Name="Alice"}; // {int:ID, string:Name}
var seq = ToEnumerable(person); // IEnumerable<{int:ID, string:Name}>

Generic Method の型推論を通じて,匿名型を型引数に持つような Generic Type を作ることができました.この Generic Type もやはり型名に匿名型を含んでいるので,キーワード var を用いるか,匿名型の名前を明記しなくてすむところまでキャストするしかありません.結局,こうやって作った Generic Type も,メソッド外部へ真の型名とともに持ち出すことはできないのです.
さて,ここで匿名型をメソッドシグネチャに持つようなラムダ式を考えてみましょう.残念ながら,以下のようには書けません.

var func = (x => new {x.ID, Name=x.Name+"_"}); // コンパイルエラー

このラムダ式は,無数のデリゲート型と,特定の Expression Tree に適合します.そのため,これだけでは func の型が 1 つに定まらないのです*3.逆にいえば,func がどのような型になればいいのかを別の方法で決めてやることで,このコードはコンパイルできるようになるというわけです.

static Func<T, T> Identity<T>(T dummy)
{
    return delegate(T arg){return arg;} ;
}

var person = new {ID=1, Name="Alice"};

// func の型は Func< {int:ID, string:Name}, {int:ID, string:Name} > と確定
var func = Identity(person);

// func の型が確定しているので,ラムダ式を何型と判断するかも定まる
func = (x => new { x.ID, Name = x.Name + "_" });

上のコードはコンパイルが可能ですが,さらにもう少し書き換えてみましょう.

static U Proj<T, U>(T source, Func<T, U> func)
{
    return func(source);
}

var person = new {ID=1, Name="Alice"};

// Proj メソッドの引数の型から,ラムダ式を何型と判断するかを定める
var person2 = Proj(person, (x => new { x.ID, Name = x.Name + "_" }));

この場合も,Generic Method である Proj の第二引数が,特定のデリゲート型であることが決まっていることから,先ほどと同様にコンパイルが可能です.
このように,ラムダ式を使用すれば,匿名型を受け取ったり匿名型を返したりするような処理を記述することができ,このことが,LINQ の select 文に柔軟な射影操作を許す鍵となっています.

コンパイル時検証に期待しない手法

さて,コンパイル時の型検証をあきらめるのであれば,次のようなコードも書くことはできます.

private static T GetBob<T>(T dummy)
{
    var y = new { ID = 2, Name = "Bob" };
    return (T)(object)y;
}

var person = new { ID = 1, Name = "Alice" };
person = GetBob(person);

しかし,呼び出し元の匿名型に新しい要素を追加して,GetBob 内の匿名型と一致しなくなると,このコードはコンパイルは通っても実行時エラーを起こすようになるでしょう.また,呼び出し元と GetBob が同じタイミングでコンパイルされていないと,GetBob 内部のキャスト操作は危険です.
コンパイル時の検証をあきらめるのであれば,匿名型を object 型として扱って,フィールド名を頼りにリフレクションで処理を記述する方法もあります.動的な O/R マップなどで,取得したいレコードの構造をインラインで示したいような場合に利用できるかもしれません.

更新履歴

  • 2008年7月4日
    • 匿名型が Immutable になった Visual C# 2008 RTM 版用に一部修正

*1:これを無名型と訳すか匿名型と訳すかは難しいところですが,コンパイラが各 AnonymousType に対して内部的な型名を与えていることを考えると,やはり匿名型でいいんじゃないかなと思います.

*2:と仕様書には書いてあるのですが,複数のアセンブリからなる CLI 処理系を考えれば,同一のコンパイル単位の中でのみ保証されると考えるべきでしょう

*3:これは,.NET デリゲートの欠点であるようにも思います.同じメソッドシグネチャを持つデリゲートは同じ型という扱いにしておけば,ここまでややこしくなることはありませんでした.次期 Java で提案されている関数型(Function types) は,この点で .NET デリゲートより好ましいように思います.http://www.javac.info/closures-v05.html