[C#][.NET][Roslyn] メタプログラミング入門 – 応用編 – オブジェクトの文字列変換のメタプログラミング (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 を使って動的に「オブジェクトを文字列に変換する」メソッドを生成するプログラムを作成した。

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