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

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

メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング (Roslyn 編)

Metasequoia

※ 「[C#][.NET][式木] メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング (式木編)」の続き。

Roslyn によるメタプログラミング

Roslyn によるメタプログラミングに関しては、以前、次にあげる記事で扱った。参考にしてほしい。

今回も、題材は同じく 「[C#][.NET] メタプログラミング入門 - 応用編 - オブジェクトの文字列変換を静的/動的に行う」の中の「(デバッグ用の) 文字列に変換」だ。

例えば、次のようなクラスのオブジェクトを文字列に変換する。

// テスト用のクラス
public sealed class Book
{
    public string Title { get; set; }
    public int    Price { get; set; }
}
Roslyn に渡す C#ソースコード

前回は、式木でラムダ式を組み立て、それをコンパイルすることにより、デリゲートを生成した。

Book クラスの場合を例にあげ、プログラムによって生成したい「文字列変換を行うラムダ式」として次のものを想定したのだった。

    // 動的に作りたいラムダ式の例 (実際のコードは targetType による):
    item => new StringBuilder().Append("Title: ").Append(item.Title)
                               .Append(", ")
                               .Append("Price: ").Append(item.Price)
                               .ToString()

対象とするオブジェクトのクラスによってラムダ式が異なるため、式木は動的に生成した。

今回は、「文字列変換を行うラムダ式」の C#ソースコードを動的に作成し、そのソースコードから Roslyn を用いてデリゲートを生成することにしよう。

先ず、Roslyn を使う前に、上のようなラムダ式C#ソースコードを、リフレクションを用いて動的に作成する。

対象とするオブジェクトとその型から、C#ソースコードを文字列として作成するメソッドを書く。 この時、リフレクションとジェネリックを用いて、型に依存しないようにする。

// ToString メソッド生成器 (Roslyn 版)
public static class ToStringGeneratorByRoslyn
{
    // ToString() メソッドの C#ソースコードを作成する
    public static string CreateCodeOfToStringByRoslyn<T>()
    {
        // 動的に作りたい C#ソースコードの例 (実際のコードは typeof(T) による):
        // item => new StringBuilder().Append("Title: ").Append(item.Title)
        //                            .Append(", ")
        //                            .Append("Price: ").Append(item.Price)
        //                            .ToString()

        var bodyCode = "new StringBuilder()" +
                       string.Join(".Append(\", \")",
                                   typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                            .Where (property => property.CanRead)
                                            .Select(property => string.Format(".Append(\"{0}: \").Append(item.{0})", property.Name))) +
                       ".ToString()";
        return string.Format("(Func<{0}, string>)(item => {1})", typeof(T).FullName, bodyCode);
    }
}

これで正しい「文字列変換を行うラムダ式」の C#ソースコードが文字列として作成されるか、Book クラスの場合で試してみよう。

using System;

static class Program
{
    static void Main()
    {
        var code = ToStringGeneratorByRoslyn.CreateCodeOfToStringByRoslyn<Book>();
        Console.WriteLine(code);
    }
}

実行してみよう。

(Func<Book, string>)(item => new StringBuilder().Append("Title: ").Append(item.Title).Append(", ").Append("Price: ").Append(item.
Price).ToString())

目的とするソースコードができているようだ。ここまでは、まだ Roslyn は使用していない。

Roslyn によるオブジェクトの文字列への変換プログラム生成プログラム

この C#ソースコードから Roslyn を使ってデリゲートを生成しよう。このやり方は、「Roslyn による Add メソッドの動的生成」や「メソッド呼び出しのパフォーマンスの比較」で行ったのと同様だ。

次のようになる。

using Roslyn.Scripting.CSharp;
using System;
using System.Linq;
using System.Reflection;

// ToString メソッド生成器 (Roslyn 版)
public static class ToStringGeneratorByRoslyn
{
    // メソッドを生成
    public static Func<T, string> Generate<T>()
    {
        var code = CreateCodeOfToStringByRoslyn<T>(); // C#ソースコードを生成
        return Generate<T>(code: code);
    }

    // Roslyn でメソッドを生成
    static Func<T, string> Generate<T>(string code)
    {
        var engine  = CreateEngine(); // スクリプトエンジン
        var session = engine.CreateSession(); // 実行するには Session が必要
        return (Func<T, string>)session.Execute(code: code); // コードの生成
    }

    // Roslyn のスクリプトエンジンを作成する
    static ScriptEngine CreateEngine()
    {
        var engine = new ScriptEngine(); // Roslyn のスクリプトエンジン
        engine.ImportNamespace(@namespace: "System"     ); // System      名前空間を using
        engine.ImportNamespace(@namespace: "System.Text"); // System.Text 名前空間を using
        engine.AddReference(typeof(ToStringGeneratorByRoslyn).Assembly); // このアセンブリ内のクラスを使用する為に参照
        return engine;
    }

    // ToString() メソッドの C#ソースコードを作成する
    static string CreateCodeOfToStringByRoslyn<T>()
    {
        // 動的に作りたい C#ソースコードの例 (実際のコードは typeof(T) による):
        // item => new StringBuilder().Append("Title: ").Append(item.Title)
        //                            .Append(", ")
        //                            .Append("Price: ").Append(item.Price)
        //                            .ToString()

        var bodyCode = "new StringBuilder()" +
                       string.Join(".Append(\", \")",
                                   typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                            .Where(property => property.CanRead)
                                            .Select(property => string.Format(".Append(\"{0}: \").Append(item.{0})", property.Name))) +
                       ".ToString()";
        return string.Format("(Func<{0}, string>)(item => {1})", typeof(T).FullName, bodyCode);
    }
}
Roslyn によるオブジェクトの文字列への変換 (キャッシュ無し)

これを使って、これまでと同様、先ずはキャッシュ無しの変換メソッドを作ろう。 次のプログラムでは、呼ばれる度に毎回コードを生成する。

// 改良前 (メソッドのキャッシュ無し)
public static class ToStringByRoslynExtensions初期型
{
    // ToString に代わる拡張メソッド (Roslyn 版)
    public static string ToStringByRoslyn初期型<T>(this T @this)
    {
        return ToStringGeneratorByRoslyn.Generate<T>()(@this);
    }
}
メソッドのキャッシュ無し版の動作テスト

次のような簡単なプログラムで動作させてみよう。

using System;

static class Program
{
    static void Main()
    {
        var book = new Book { Title = "Metaprogramming C#", Price = 3200 };

        Console.WriteLine(book.ToStringByRoslyn初期型());
    }
}

実行結果は次のようになり、正しく動作する。

Title: Metaprogramming C#, Price: 3200
生成したメソッドのキャッシュ

では、キャッシュを利用してみよう。

今回もメソッド キャッシュ クラスを使う。

using System;
using System.Collections.Generic;

//  生成したメソッド用のキャッシュ
public class MethodCache<TResult>
{
    // メソッド格納用
    readonly Dictionary<Type, Delegate> methods = new Dictionary<Type, Delegate>();

    // メソッドの呼び出し (メソッド生成用のメソッドを引数 generator として受け取る)
    public TResult Call<T>(T item, Func<Func<T, TResult>> generator)
    {
        return Get<T>(generator)(item); // キャッシュにあるメソッドを呼び出す
    }

    // メソッドをキャッシュを介して取得 (メソッド生成用のメソッドを引数 generator として受け取る)
    Func<T, TResult> Get<T>(Func<Func<T, TResult>> generator)
    {
        var      targetType = typeof(T);
        Delegate method;
        if (!methods.TryGetValue(key: targetType, value: out method)) { // キャッシュに無い場合は
            method = generator();                                       // 動的にメソッドを生成して
            methods.Add(key: targetType, value: method);                // キャッシュに格納
        }
        return (Func<T, TResult>)method;
    }
}
Roslyn によるオブジェクトの文字列への変換 (キャッシュ有り)

では、キャッシュを行う「オブジェクトの文字列への変換」を作成しよう。

上のメソッドキャッシュ クラス MethodCache を利用して、次のようにする。

// 改良後 (メソッドのキャッシュ有り)
public static class ToStringByRoslynExtensions改
{
    // 生成したメソッドのキャッシュ
    static readonly MethodCache<string> toStringCache = new MethodCache<string>();

    // ToString に代わる拡張メソッド (Roslyn 版)
    public static string ToStringByRoslyn改<T>(this T @this)
    {
        // キャッシュを利用してメソッドを呼ぶ
        return toStringCache.Call(item: @this, generator: ToStringGeneratorByRoslyn.Generate<T>);
    }
}
メソッドのキャッシュ有り版の動作テスト

こちらも動作させてみよう。

using System;

static class Program
{
    static void Main()
    {
        var book = new Book { Title = "Metaprogramming C#", Price = 3200 };

        Console.WriteLine(book.ToStringByRoslyn改());
    }
}

やはり、実行結果は同じだ。

Title: Metaprogramming C#, Price: 3200

まとめ

今回は、前回の式木を用いた方法に続き、Roslyn を使って動的に「オブジェクトを文字列に変換する」メソッドを生成するプログラムを作成した。

次回は、メタプログラミングによる文字列変換のまとめとして、それぞれの方法でのパフォーマンスの比較を行う。