メタプログラミング事始め

はじめに

メタプログラミング.NET』を読んで、 C# で黒魔術もといメタプログラミングを習得したくなってしまったので、 まずは簡単なお題で練習してみた。

メタプログラミング.NET (アスキー書籍)

メタプログラミング.NET (アスキー書籍)

お題

任意のクラスのインスタンスを生成し Dictionary<string, object> に格納された値で初期化するファクトリーメソッドを作成する。

例えば

public class Product
{
    public long Id { get; set; }

    public string Code { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set; }
}

のようなクラスがある場合に

public static Product Create(IDictionary<string, object> data)
{
    var product = new Product();
    product.Id = (long)data["Id"];
    product.Code = (string)data["Code"];
    product.Name = (string)data["Name"];
    product.Price = (decimal)data["Price"];
    return product;
}

という感じのファクトリーメソッドを動的に生成するのが目標。

リフレクション

まずは素直にリフレクションを使ってみる。

 public static T CreateUsingRefrection<T>(IDictionary<string, object> data)
 {
     var obj = Activator.CreateInstance<T>();

     var type = typeof(T);
     var properties = type.GetProperties(
         BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
     foreach (var p in properties)
     {
         var value = data[p.Name];
         var convertedValue = Convert.ChangeType(value, p.PropertyType);
         p.SetValue(obj, convertedValue);
     }

     return obj;
 }

動的に生成していないけど、分かりやすいので、 パフォーマンスを求められない部分ならこれでもいいかと思ってる。

Expression

お次は System.Linq.Expressions にある Expression を使って、動的にデリケートを生成してみる。

public static Func<IDictionary<string, object>, T> CreateFuncUsingExpression<T>()
{
    var resultType = typeof(T);

    // IDictionary<string, object> data;
    var data = Expression.Parameter(typeof(IDictionary<string, object>), "data");

    // T result;
    var result = Expression.Parameter(resultType, "result");

    var expressions = new List<Expression>();

    // result = new T();
    expressions.Add(
        Expression.Assign(
            result,
            Expression.New(resultType)));

    var properties = resultType.GetProperties(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
    foreach (var p in properties)
    {
        // result.Property = (PropertyType)data.Item["Property"];
        // を組み立てる
        expressions.Add(
            Expression.Assign(
                Expression.PropertyOrField(
                    result,
                    p.Name),
                Expression.Convert(
                    Expression.Property(
                        data,
                        "Item",
                        Expression.Constant(p.Name)),
                    p.PropertyType)));
    }

    expressions.Add(result);

    var body = Expression.Block(
        resultType,
        new[] { result },
        expressions);

    var e = Expression.Lambda<Func<IDictionary<string, object>, T>>(
        body,
        data);

    var f = e.Compile();
    return f;
}

メソッド呼び出すだけなら Expression で良いけど、ブロックが必要になった場合はシンドイ。デバッガを使ったカンニングがそのまま使えず苦労した。

Refrection.Emit

最後は Refrection.Emit を使い、IL 手書きで動的デリゲート生成。

public static Func<IDictionary<string, object>, T> CreateFuncUsingRefrectionEmit<T>()
{
    var resultType = typeof(T);
    var resultConstructor = resultType.GetConstructor(new Type[0]);
    var resultProperties = resultType.GetProperties(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

    var argType = typeof(IDictionary<string, object>);
    var itemProperty = argType.GetProperty(
        "Item",
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);


    var dm = new DynamicMethod(
        Guid.NewGuid().ToString("N"),
        resultType,
        new[] { argType });

    var il = dm.GetILGenerator();

    il.Emit(OpCodes.Newobj, resultConstructor);

    foreach (var p in resultProperties)
    {
        // result.Property = (PropertyType)arg.Item["Property"];
        // を組み立てる。
        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldstr, p.Name);
        il.Emit(OpCodes.Callvirt, itemProperty.GetMethod);
        if (p.PropertyType.IsValueType)
        {
            // 値型のときはボックス化解除
            il.Emit(OpCodes.Unbox_Any, p.PropertyType);
        }
        else
        {
            // クラスのときはキャスト
            il.Emit(OpCodes.Castclass, p.PropertyType);
        }
        il.Emit(OpCodes.Callvirt, p.SetMethod);
    }

    il.Emit(OpCodes.Ret);

    var f = (Func<IDictionary<string, object>, T>)dm.CreateDelegate(typeof(Func<IDictionary<string, object>, T>));
    return f;
}

Expression で苦労したブロックも、IL 手書きだと意外にスンナリ書けた。

コード全体

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

namespace BlackMagicBenchmarks
{
    public class RefrectionVsExpressionVsIL
    {
        IDictionary<string, object> data;
        Func<IDictionary<string, object>, Product> expression;
        Func<IDictionary<string, object>, Product> refrectionEmit;

        [GlobalSetup]
        public void Setup()
        {
            data = new Dictionary<string, object>()
            {
                ["Id"] = 1L,
                ["Code"] = "0001",
                ["Name"] = "Visual Studio",
                ["Price"] = 100000m,
            };
            expression = ProductFactory.CreateFuncUsingExpression<Product>();
            refrectionEmit = ProductFactory.CreateFuncUsingRefrectionEmit<Product>();
        }

        [Benchmark]
        public Product Manual()
        {
            return ProductFactory.Create(data);
        }

        [Benchmark]
        public Product Refrection()
        {
            return ProductFactory.CreateUsingRefrection<Product>(data);
        }

        [Benchmark]
        public Product Expression()
        {
            return expression(data);
        }

        [Benchmark]
        public Product RefrectionEmit()
        {
            return refrectionEmit(data);
        }
    }

    public static class ProductFactory
    {
        public static Product Create(IDictionary<string, object> data)
        {
            var product = new Product();
            product.Id = (long)data["Id"];
            product.Code = (string)data["Code"];
            product.Name = (string)data["Name"];
            product.Price = (decimal)data["Price"];
            return product;
        }

        public static T CreateUsingRefrection<T>(IDictionary<string, object> data)
        {
            var obj = Activator.CreateInstance<T>();

            var type = typeof(T);
            var properties = type.GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
            foreach (var p in properties)
            {
                var value = data[p.Name];
                var convertedValue = Convert.ChangeType(value, p.PropertyType);
                p.SetValue(obj, convertedValue);
            }

            return obj;
        }

        public static Func<IDictionary<string, object>, T> CreateFuncUsingExpression<T>()
        {
            var resultType = typeof(T);

            // IDictionary<string, object> data;
            var data = Expression.Parameter(typeof(IDictionary<string, object>), "data");

            // T result;
            var result = Expression.Parameter(resultType, "result");

            var expressions = new List<Expression>();

            // result = new T();
            expressions.Add(
                Expression.Assign(
                    result,
                    Expression.New(resultType)));

            var properties = resultType.GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
            foreach (var p in properties)
            {
                // result.Property = (PropertyType)data.Item["Property"];
                // を組み立てる
                expressions.Add(
                    Expression.Assign(
                        Expression.PropertyOrField(
                            result,
                            p.Name),
                        Expression.Convert(
                            Expression.Property(
                                data,
                                "Item",
                                Expression.Constant(p.Name)),
                            p.PropertyType)));
            }

            expressions.Add(result);

            var body = Expression.Block(
                resultType,
                new[] { result },
                expressions);

            var e = Expression.Lambda<Func<IDictionary<string, object>, T>>(
                body,
                data);

            var f = e.Compile();
            return f;
        }

        public static Func<IDictionary<string, object>, T> CreateFuncUsingRefrectionEmit<T>()
        {
            var resultType = typeof(T);
            var resultConstructor = resultType.GetConstructor(new Type[0]);
            var resultProperties = resultType.GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

            var argType = typeof(IDictionary<string, object>);
            var itemProperty = argType.GetProperty(
                "Item",
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);


            var dm = new DynamicMethod(
                Guid.NewGuid().ToString("N"),
                resultType,
                new[] { argType });

            var il = dm.GetILGenerator();

            il.Emit(OpCodes.Newobj, resultConstructor);

            foreach (var p in resultProperties)
            {
                // result.Property = (PropertyType)arg.Item["Property"];
                // を組み立てる。
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldstr, p.Name);
                il.Emit(OpCodes.Callvirt, itemProperty.GetMethod);
                if (p.PropertyType.IsValueType)
                {
                    // 値型のときはボックス化解除
                    il.Emit(OpCodes.Unbox_Any, p.PropertyType);
                }
                else
                {
                    // クラスのときはキャスト
                    il.Emit(OpCodes.Castclass, p.PropertyType);
                }
                il.Emit(OpCodes.Callvirt, p.SetMethod);
            }

            il.Emit(OpCodes.Ret);

            var f = (Func<IDictionary<string, object>, T>)dm.CreateDelegate(typeof(Func<IDictionary<string, object>, T>));
            return f;
        }
    }

    public class Product
    {
        public long Id { get; set; }

        public string Code { get; set; }

        public string Name { get; set; }

        public decimal Price { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<RefrectionVsExpressionVsIL>();
        }
    }
}

ベンチマーク

BenchmarkDotNet を使ってベンチマークをとってみた。

f:id:griefworker:20180416172552p:plain

リフレクションは桁違いに遅い。 Expression と Refrection.Emit は大差無い。

おわりに

Expression ならデバッガー、Reflection.Emit ではアセンブリを dnSpy で解析することでカンニングできる。 一から全部自分で書かなくてもいい。 今回のケースでは、Expression よりも Reflection.Emit の方が書きやすかった。 Reflection.Emit に慣れたら、Expression を使うことは無くなるかもな。