ややニッチな Anonymous Types の使い方をまとめてみる (C# 3.0)

@IT に連載されている川俣さんの記事から一年前のうちの日記のエントリがリンクされていた.当時の日記を読み直して,ほんの一年前まで Anonymous Types が Immutable かどうかといった重要な仕様が不確かだったことを思い出す.C# 3.0 がリリースされてからだいぶ経ったような気がしていたけど,よくよく考えるとまだ半年ちょっとしか経っていないのだなぁ.
それはそれとして,川俣さんの記事に「匿名型の使用目的」というトピックがあったのが目にとまった.うちの日記でも王道チックな話は RTM までに延々書き散らかしてきたが,本筋から外れたニッチな用途についてまとめたことはなかったように思う.そこで,ここ一年ぐらいで見かけた少しニッチで新しめの Anonymous Types の使い方について改めてまとめてみることにする.

簡易ハッシュテーブル記法として使う

ASP.Net MVC Framework で有名になったのが Anonymous Types を簡易ハッシュテーブルのように使う方法だ.

routes.MapRoute(
               "Default",                                              // Route name
               "{controller}/{action}/{id}",                           // URL with parameters
               new { controller = "Home", action = "Index", id = "" }, // Parameter defaults
               new { controller = @"[^\.]*" }                          // Parameter constraints
           );

意図はまあ見ての通り.
名前付き省略可能引数とかハッシュテーブルそのものと絡めて語っても良いだろう.オブジェクト初期化子でも似たようなことができる.
意味論的に深い意味があるかというとそんなものはなさそうだけど,C# でのシンタックス遊びとしては今後目にする機会が増えてくるだろうと思っている.
ただし,識別子に許される文字種で困る可能性はある.ハッシュテーブルには "/bin/sh" というキーを登録出来るが,Anonymous Types の識別子には使えない.
過去の関連記事.

Functional Construction の代わりに使う

本題に進む前に Functional Construction について知っておく必要がある.
Functional Construction は,C# の文法的には特殊な記法ではない.ただ,LINQ to XML で使われているため,今後他のライブラリでも採用される可能性がある.

関数型構築 (LINQ to XML)

LINQ to XML には、関数型構築と呼ばれる強力な XML 要素作成機能があります。関数型構築は、単一のステートメントで XML ツリーを作成するための機能です。

LINQ to XML プログラミング インターフェイスには、関数型構築を利用するためのいくつかの重要な機能があります。

  • XElement コンストラクタは、さまざまな種類のコンテンツ引数を受け取ります。たとえばこのコンストラクタに対して、子要素になる別の XElement オブジェクトや、要素の属性になる XAttribute オブジェクトを渡すことができます。また、文字列に変換され、要素のテキスト コンテンツになる他の任意の種類のオブジェクトを渡すこともできます。
  • XElement コンストラクタは、Object 型の params 配列を受け取ります。そのため、任意の数のオブジェクトを配列に渡すことができます。これにより、複雑なコンテンツを持つ要素を作成できます。
  • オブジェクトが IEnumerable<T> を実装している場合、オブジェクト内のコレクションが列挙され、コレクション内のすべての項目が追加されます。コレクションに XElement オブジェクトまたは XAttribute オブジェクトが含まれている場合、コレクション内の各項目が個別に追加されます。これは重要な機能といえます。その理由は、この機能により、LINQ クエリの結果をコンストラクタに渡すことができるためです。

これらの機能により、XML ツリーを作成するコードを記述することができます。次に例を示します。

XElement contacts =
    new XElement("Contacts",
        new XElement("Contact",
            new XElement("Name", "Patrick Hines"),
            new XElement("Phone", "206-555-0144"),
            new XElement("Address",
                new XElement("Street1", "123 Main St"),
                new XElement("City", "Mercer Island"),
                new XElement("State", "WA"),
                new XElement("Postal", "68042"))));

この代用として,Anonymous Types を使うことができる.

var contacts = new 
{
    Contacts = new
    {
        Contact = new
        {
            Name = "Patrick Hines",
            Phone = "206-555-0144",
            Address = new
            {
                Street1 = "123 Main St",
                City = "Mercer Island",
                State = "WA",
                Postal = "68042",
            },
        },
    },
};

実際,Json.NET では以下のような記述で JSON 形式への変換を行うことができる.

JObject o = JObject.FromObject(new
{
    Image = new
    {
        Width = 800,
        Height = 640,
        Title = "View from 15th Floor",
        Thumbnail = new
        {
            Url = "http://www.example.com/image/481989943",
            Height = 125,
            Width = "100",
        },
    },
    IDs = new[] { 116, 943, 234, 38793 },
});
Console.WriteLine(o.ToString());

これを実行すると以下のように出力される.

{
  "Image": {
    "Width": 800,
    "Height": 640,
    "Title": "View from 15th Floor",
    "Thumbnail": {
      "Url": "http://www.example.com/image/481989943",
      "Height": 125,
      "Width": "100"
    }
  },
  "IDs": [
    116,
    943,
    234,
    38793
  ]
}

しかし,ここでも識別子に許される文字種の制限で困る可能性はある.例えば JSON では "$PATH" は有効なキーだが,Anonymous Types では識別子に使えない.
(追記)『WPF メモ - まめしば雑記』を読んで,WPF のデータバインディングへの応用も本来大きく採り上げるべきだったことに気付いた.後で加筆するかも.

デリゲート型のメンバをメソッド的に利用する

Anonymous Types に定義したデリゲート型のメンバは,メソッドのように見える.これを応用したのが『An implementation of duck-typing by Anonymous Typess - NyaRuRuの日記』で示した duck typing の実装だ.
以下の Anonymous Types は,"Bark" という名前の,Action 型のメンバを持つ.

var obj = new { Bark = default(Action) };

このように定義された obj は,以下のようなシンタックスで利用できる.

obj.Bark();

次のようなふたつのクラスを考えよう.

public class Duck
{
    public void Bark()
    {
        Console.WriteLine("gaaa");
    }
}
public class Human
{
    public void Bark()
    {
        Console.WriteLine("hauhau");
    }
}

ふたつのクラスはいずれも Action 型でキャプチャ可能な "Bark" という名前のメソッドを持つ.ここに注目し,次のような動作を行う関数 BindByName を作るのは,不可能ではない.

var obj1 = BindByName( new Duck(), new { Bark = default(Action) } );
var obj2 = BindByName( new Human(), new { Bark = default(Action) } );

obj1.Bark(); // "gaaa"
obj2.Bark(); // "hauhau"

Assert.IsTrue(obj1.GetType() == new { Bark = default(Action) }.GetType());
Assert.IsTrue(obj1.GetType() == obj2.GetType());

このアプローチを一般化すると,型グラフ的には互換性のない複数の型から,Anonymous Types で定義された構造を利用して共通部分のみを抽出するという戦略が見えてくる.次はその一般系について考えてみよう.

構造を利用してデータを抽出する

Anonymous Types は,パターンマッチングや Query-by-Example への応用が可能だ.例えば,obj.Image.Width == 800 && obj.Image.Thumbnail.Width == "100" という構造と値を持つ JSON オブジェクトを列挙する構文として,以下のようなものが原理的に可能である(ただし,実際にこのような記法が可能な C# のライブラリはまだ見たことがない).

IEnumerable<JObject> jObjects = ...
var count = jObjects.Like(() => new
                     {
                         Image = new
                         {
                             Width = 800,
                             Thumbnail = new
                             {
                                 Width = "100",
                             },
                         },
                     })
                    .Count();

複数条件の指定についてはどうだろうか? 考えられる方法のひとつは,Like メソッドの引数を Expression Trees にして,以下のような記法を導入するというものだ.

IEnumerable<JObject> jObjects = ...
var query = jObjects.Like(() => new
                     {
                         Image = new
                         {
                             Width = Patterns.Or(800, 640, 400),
                             Thumbnail = new
                             {
                                 Width = "100",
                             },
                         },
                     })
                    .Select(obj => obj.Image.Width); // 実際にキャプチャされた値が使える

Expression Trees を解析すれば,obj.Image.Width 要素が Patterns.Or(800, 640, 400) という式によって初期化されることがわかる.この構造を利用して複雑な条件をエンコードすることが可能だろう.この例であれば,obj.Image.Width が 800,640,400 のいずれかのときのみマッチし,実際の値はこのメンバに格納されることにすればよい.これは条件付きのキャプチャにあたる.
条件を指定しないキャプチャも,同じ要領で可能だ.

IEnumerable<JObject> jObjects = ...
var query = jObjects.Like(() => new
                     {
                         Image = new
                         {
                             Width = Patterns.Or(800, 640, 400),
                             Thumbnail = new
                             {
                                 Width = Patterns.Any<string>(),
                             },
                         },
                     })
                    .Select(obj => obj.Image.Thumbnail.Width);

Width の初期化に使用している Patterns.Any が今回のキーとなる.この式に込める意図としては,obj.Image.Thumbnail.Width は string 型であれば何でも良く,結果はそこにキャプチャされる事にすればよい.
こういった,関数を利用したパターン記述は応用が利く.ここに挙げた Any や Or 以外でも,特定の条件が満たされるときのみキャプチャされる Patterns.If や,キャプチャ時に射影処理を行う Patterns.Convert といったものが定義できるだろう.

まとめ

以上,私がここ 1 年ぐらいの間に注目するようになった Anonymous Types の使用法を紹介してみた.LINQ の導入が一段落し,何か新しいことに挑戦してみたいという方の参考になれば幸いである.
ちなみに現在私が最も注目しているのは,最近この日記でパターンマッチに関する記事が増えていることからも分かるように,最後に紹介したパターン記述構文としての Anonymous Types の応用である.同様の事例を探してみたところ,以下の Metaweb Query API が参考になりそうだ.