メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング (Roslyn 編)
※ 「[C#][.NET][式木] メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング (式木編)」の続き。
Roslyn によるメタプログラミング
Roslyn によるメタプログラミングに関しては、以前、次にあげる記事で扱った。参考にしてほしい。
- [C#][.NET][Roslyn] メタプログラミング入門 - Roslyn による Add メソッドの動的生成
- [C#][.NET] メタプログラミング入門 - メソッド呼び出しのパフォーマンスの比較
今回も、題材は同じく 「[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 を使って動的に「オブジェクトを文字列に変換する」メソッドを生成するプログラムを作成した。
次回は、メタプログラミングによる文字列変換のまとめとして、それぞれの方法でのパフォーマンスの比較を行う。