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

プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

メタプログラミング入門 - 応用編 - オブジェクトの文字列変換を静的/動的に行う

.NET C#
Metasequoia

※ 「[C#][.NET] メタプログラミング入門 - メソッド呼び出しのパフォーマンスの比較」の続き。

前回は、コード生成を行ってメソッド呼び出しを行う3通りの方法と静的なメソッド呼び出しや動的なメソッド呼び出しのパフォーマンスを比較した。

今回から、少しずつ応用に入っていきたい。

■ メタプログラミングの応用例

[C#][.NET] メタプログラミング入門 - はじめに」で、メタプログラミングが有効な例として次のようなものがあると述べた。

メタプログラミングが有効な例
  • コンパイラー/インタープリター
    ホスト言語のソースコードから動的に対象言語のプログラムを生成
  • O/R マッパー
    クラスやオブジェクトから動的に SQL を生成
  • XML や JSON の入出力
    クラスやオブジェクト等から動的に XMLJSON を生成/XML や JSON から動的にクラスやオブジェクト等を生成
    (生成するプログラムをプログラムで生成)
  • モック (mock) オブジェクト
    モック (ユニットテストで用いられる代用のオブジェクト) を動的に生成
    (生成するプログラムをプログラムで生成)
  • Web アプリケーション
    クライアント側で動作するプログラム (HTMLJavaScript 等) をサーバー側で動的に生成

この中から、今回は、「クラスやオブジェクト等から動的に文字列を生成する例」として、次にあげる例を見てみることにしよう。

クラスやオブジェクト等から動的に文字列を生成する例
  • デバッグ情報の出力
    クラスやオブジェクト等を、型情報を使って文字列に変換し、テストやデバッグで用いる。
  • ASP.NET (Web アプリケーション/Web サービス) のサーバー側の処理
    クラスやオブジェクト等を、型情報を使って HTML に変換し、出力する。
  • XMLリアライザー
    クラスやオブジェクト等を、型情報を使って XML に変換し、出力する。

これらについてメタプログラミング (動的コード生成) の例を示す前に、メタプログラミングによらない静的な方法とリフレクションによる動的な方法を見てみよう。

オブジェクトから文字列を生成する静的な例

先ずは、基本である静的な方法だ。

テスト用のクラス

文字列に変換する対象のクラスとして、次の単純な Book クラスを用意した。

// テスト用のクラス
public sealed class Book
{
    public string Title { get; set; }
    public int    Price { get; set; }
}

この Book クラスに対して、次の3通りを行うメソッドをそれぞれ作成しよう。

  • オブジェクトを、(デバッグ出力用の) 文字列に変換
  • オブジェクトを、XML に変換
  • オブジェクトを、HTML に変換
(デバッグ出力用の) 文字列に変換

これは、単純に文字列に変換するメソッドだ。Book クラスで ToString() メソッドをオーバーライドすることにする。

using System.Text;

// テスト用のクラス
public sealed class Book
{
    public string Title { get; set; }
    public int    Price { get; set; }

    // 文字列へ変換するメソッド ToString() をオーバーライド
    public override string ToString()
    {
        return new StringBuilder()                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
                   .Append("Title: ").Append(Title)
                   .Append(", "     )
                   .Append("Price: ").Append(Price)
                   .ToString();

        // または:
        //return string.Format("Title: {0}, Price: {1}", Title, Price);
    }
}
XML や HTML への変換

次は、XML や HTML に変換するメソッドだ。

XML への変換の拡張メソッドと HTML の table への変換の拡張メソッドを、それぞれ作成してみる。

using System.Collections.Generic;
using System.Text;

public static class BookExtensions
{
    // XML へ変換
    public static string ToXml(this IEnumerable<Book> @this)
    {
        var stringBuilder = new StringBuilder();
        stringBuilder.Append("<?xml version=\"1.0\"?>");
        @this.ForEach(
            book =>
               stringBuilder.Append("<Book>" )
                            .Append("<Title>").Append(book.Title).Append("</Title>")
                            .Append("<Price>").Append(book.Price).Append("</Price>")
                            .Append("</Book>")
        );
        return stringBuilder.ToString();
    }

    // HTML の table へ変換
    public static string ToHtmlTable(this IEnumerable<Book> @this)
    {
        var stringBuilder = new StringBuilder();
        stringBuilder.Append("<table><thead><tr><th>Title</th><th>Price</th></tr></thead><tbody>");
        @this.ForEach(
            book => stringBuilder.Append("<tr><td>").Append(book.Title).Append("</td>"     )
                                 .Append(    "<td>").Append(book.Price).Append("</td></tr>"));
        return stringBuilder.Append("</tbody></table>").ToString();
    }
}

上のコードで呼び出している ForEach メソッドは別クラスに拡張メソッドとして用意した。

using System;
using System.Collections.Generic;

// ForEach 用
public static class EnumerableExtensions
{
    public static void ForEach<T>(this IEnumerable<T> @this, Action<T> action)
    {
        foreach (var item in @this)
            action(item);
    }
}
文字列を生成する静的な例のテスト

それでは、これらのメソッドを呼び出してみよう。

using System;
using System.Reflection;

static class Program
{
    static void Main()
    {
        文字列への変換のテスト();
        XMLへの変換のテスト   ();
        HTMLへの変換のテスト  ();
    }

    static void 文字列への変換のテスト()
    {
        Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示

        var book = new Book { Title = "Metaprogramming C#", Price = 3200 };

        Console.WriteLine(book.ToString());
    }

    static void XMLへの変換のテスト()
    {
        Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示

        var books = new {
            new Book { Title = "Metaprogramming C#", Price = 3200 },
            new Book { Title = "Metaprogramming VB", Price = 2100 },
            new Book { Title = "Metaprogramming F#", Price = 4300 }
        };

        Console.WriteLine(books.ToXml());
    }

    static void HTMLへの変換のテスト()
    {
        Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示

        var books = new {
            new Book { Title = "Metaprogramming C#", Price = 3200 },
            new Book { Title = "Metaprogramming VB", Price = 2100 },
            new Book { Title = "Metaprogramming F#", Price = 4300 }
        };

        Console.WriteLine(books.ToHtmlTable());
    }
}

実行結果は次のようになる。

【文字列への変換のテスト】
Title: Metaprogramming C#, Price: 3200
【XMLへの変換のテスト】
<?xml version="1.0"?><Book><Title>Metaprogramming C#</Title><Price>3200</Price></Book><Book><Title>Metaprogramming VB</Title><Pri
ce>2100</Price></Book><Book><Title>Metaprogramming F#</Title><Price>4300</Price></Book>
【HTMLへの変換のテスト】
<table><thead><tr><th>Title</th><th>Price</th></tr></thead><tbody><tr><td>Metaprogramming C#</td><td>3200</td></tr><tr><td>Metapr
ogramming VB</td><td>2100</td></tr><tr><td>Metaprogramming F#</td><td>4300</td></tr></tbody></table>

単純なデバッグ用の文字列と XML、HTML が出力されている。

オブジェクトから文字列を生成する動的な例 (リフレクションを使った例)

上記の静的な方法には、大きな問題点がある。これでは、対象とする型ごとに手でコードを書かなければならない。

上では Book クラスを用いて試しているが、Book クラス以外のクラスを対象とする場合には、また同様の3つのメソッドを作る必要があるだろう。

そこで、そのようなことにならないように、今度は、型情報から動的に処理を行うようにしてみよう。リフレクションを使ってみる。

リフレクションを使ってオブジェクトを動的に文字列や XML、HTML に変換

実際のコードは次のようになるだろう。静的な場合と比べてやや複雑になる。

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

// リフレクションによる型によらない各種文字列への変換 (リフレクション版)
public static class ToStringByReflectionExtensions
{
    // 文字列へ変換 (リフレクション版)
    public static string ToStringByReflection<T>(this T @this)
    {
        // リフレクションで public なインスタンス プロパティの情報を全て取得
        var properties = @this.GetType().GetProperties(bindingAttr: BindingFlags.Instance | BindingFlags.Public);
        // LINQ でプロパティの情報の集まりから、読み込み可能なものに絞り込み、名前と値から作った文字列の集まりにする
        var textCollection = properties
                             .Where(property => property.CanRead)
                             .Select(property => string.Format("{0}: {1}", property.Name, property.GetValue(@this, null)));
        // string.Join で連結
        return string.Join(separator: ", ", values: textCollection);
    }

    // XML へ変換 (リフレクション版)
    public static string ToXmlByReflection<T>(this IEnumerable<T> @this)
    {
        var stringBuilder = new StringBuilder();
        stringBuilder.Append("<?xml version=\"1.0\"?>");

        var itemType      = typeof(T);

        // 要素の型のプロパティの一覧を取得
        var properties    = itemType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                    .Where(property => property.CanRead);

        @this.ForEach(
            // 要素毎の XML
            item => {
                stringBuilder.Append("<" ).Append(itemType.Name).Append(">");
                properties.ForEach(
                    // 要素のプロパティ毎の XML
                    property => stringBuilder.Append("<" ).Append(property.Name).Append(">")
                                             .Append(property.GetValue(item))
                                             .Append("</").Append(property.Name).Append(">")
                );
                stringBuilder.Append("</").Append(itemType.Name).Append(">");
            }
        );
        return stringBuilder.ToString();
    }

    // HTML の table へ変換 (リフレクション版)
    public static string ToHtmlTableByReflection<T>(this IEnumerable<T> @this)
    {
        var itemType      = typeof(T);

        // 要素の型のプロパティの一覧を取得
        var properties    = itemType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                    .Where(property => property.CanRead);
        if (properties.Count() == 0)
            return string.Empty;

        var stringBuilder = new StringBuilder();

        // table のヘッダー部の追加
        stringBuilder.Append("<table><thead><tr>");
        properties.ForEach(property => stringBuilder.Append("<th>").Append(property.Name).Append("</th>"));
        stringBuilder.Append("</tr></thead>");

        // table の本体を追加
        stringBuilder.Append("<tbody>");
        @this.ForEach(
            item => {
                stringBuilder.Append("<tr>");
                properties.Where(property => property.CanRead)
                          .ForEach(
                               property =>
                                   stringBuilder.Append("<td>").Append(property.GetValue(item)).Append("</td>")
                           );
                stringBuilder.Append("</tr>");
            }
        );
        return stringBuilder.Append("</tbody></table>").ToString();
    }
}

コードでは、ジェネリック プログラミングによって型への依存を無くしている。また、リフレクションで動的に型情報を利用することでも対象とするオブジェクトの型に依存しない処理を実現している。

こうすることにより、Book 以外のクラスのオブジェクトにも用いることができ、対象とするオブジェクトの型ごとにコードを書かなくて済む訳だ。

文字列を生成するリフレクションによる動的な例のテスト

こちらも呼び出してみよう。先の Program クラスを少し書き換えてリフレクション版のメソッドを呼ぶようにしてみる。

using System;
using System.Reflection;

static class Program
{
    static void Main()
    {
        文字列への変換のテスト();
        XMLへの変換のテスト   ();
        HTMLへの変換のテスト  ();
    }

    static void 文字列への変換のテスト()
    {
        Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示

        var book = new Book { Title = "Metaprogramming C#", Price = 3200 };

        Console.WriteLine(book.ToStringByReflection());
    }

    static void XMLへの変換のテスト()
    {
        Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示

        var books = new {
            new Book { Title = "Metaprogramming C#", Price = 3200 },
            new Book { Title = "Metaprogramming VB", Price = 2100 },
            new Book { Title = "Metaprogramming F#", Price = 4300 }
        };

        Console.WriteLine(books.ToXmlByReflection());
    }

    static void HTMLへの変換のテスト()
    {
        Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示

        var books = new {
            new Book { Title = "Metaprogramming C#", Price = 3200 },
            new Book { Title = "Metaprogramming VB", Price = 2100 },
            new Book { Title = "Metaprogramming F#", Price = 4300 }
        };

        Console.WriteLine(books.ToHtmlTableByReflection());
    }
}

実行してみると、結果は次のように静的な場合と変わらない。

【文字列への変換のテスト】
Title: Metaprogramming C#, Price: 3200
【XMLへの変換のテスト】
<?xml version="1.0"?><Book><Title>Metaprogramming C#</Title><Price>3200</Price></Book><Book><Title>Metaprogramming VB</Title><Pri
ce>2100</Price></Book><Book><Title>Metaprogramming F#</Title><Price>4300</Price></Book>
【HTMLへの変換のテスト】
<table><thead><tr><th>Title</th><th>Price</th></tr></thead><tbody><tr><td>Metaprogramming C#</td><td>3200</td></tr><tr><td>Metapr
ogramming VB</td><td>2100</td></tr><tr><td>Metaprogramming F#</td><td>4300</td></tr></tbody></table>

まとめ

ここまで、オブジェクトを文字列に変換する静的な方法と動的な方法を示した。動的な方法では、リフレクションを使うことにより柔軟な処理を行うことができた。

しかし、リフレクションには、前回試したように実行速度が遅い、という欠点がある。

次回からは、メタプログラミングを行い、動的にコードを生成する方法を試していきたい。