C#でもリフレクションで ToString したい

はじめに

JavaにReflectionToStringBuilderってあるじゃないですか。 Apache Commonsのアレです。 特に需要があるわけではないですが、どうしてもそれを.NETでやりたくなったのでC#で書いてみました。

リフレクション

とりあえず文字列に含むのはインデックス付きでない読み取り可能なパブリックなプロパティとします。

というわけでPropertyInfoを使ったパターンはこちら。

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

namespace ReflectionToStringBuilder
{
    public class PropertyInfoToStringBuilder
    {
        private static ConcurrentDictionary<Type, IEnumerable<PropertyInfo>> _cache
            = new ConcurrentDictionary<Type, IEnumerable<PropertyInfo>>();

        public static string DynamicToString<T>(T value)
        {
            var objectType = typeof(T);
            IEnumerable<PropertyInfo> propInfo;

            if (!_cache.TryGetValue(objectType, out propInfo))
            {
                propInfo = objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
                _cache.TryAdd(objectType, propInfo);
            }
            
            var toStringProp = propInfo
                .Where((it) => it.CanRead)
                .Where((it) => it.GetIndexParameters().Length == 0);

            var text = new StringBuilder();
            text.Append($"{objectType.Name}{{");

            foreach(var it in toStringProp)
            {
                text.Append($"{it.Name}={it.GetValue(value)?.ToString() ?? "null"},");
            }
            text.Append("}");

            return text.ToString();
        }
    }
}

まぁ、どうってことのないコードですね。特に説明の要らないくらいですね。

とりあえずPropertyInfoをキャッシュしてるんですが、コレ意味あるんですかね?

式木

もリフレクションだとあんまりひねりが無いですよね。

なのでナウでヤングな式木を使って高速化を図ってみました。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace ReflectionToStringBuilder
{
    public class ExprTreeToStringBuilder
    {
        private static ConcurrentDictionary<Type, IEnumerable<Tuple<string, Func<object, object>>>> _cache
            = new ConcurrentDictionary<Type, IEnumerable<Tuple<string, Func<object, object>>>>();

        public static string DynamicToString<T>(T value)
        {
            var objectType = typeof(T);
            IEnumerable<Tuple<string, Func<object, object>>> exprs;
            if(!_cache.TryGetValue(objectType, out exprs))
            {
                exprs = BuildAccessor(objectType);
                _cache.TryAdd(objectType, exprs);
            }

            var text = new StringBuilder();
            text.Append($"{objectType.Name}{{");

            foreach (var it in exprs)
            {
                text.Append($"{it.Item1}={it.Item2(value)?.ToString() ?? "null"},");
            }
            text.Append("}");

            return text.ToString();
        }

        private static IEnumerable<Tuple<string, Func<object, object>>> BuildAccessor(Type targetType)
        {
            var toStringProp = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where((it) => it.CanRead)
                .Where((it) => it.GetIndexParameters().Length == 0);

            var result = new List<Tuple<string, Func<object, object>>>();

            foreach(var it in toStringProp)
            {
                var arg = Expression.Parameter(typeof(object), "it");
                var convToTarget = Expression.Convert(arg, targetType);
                var getPropValue = Expression.MakeMemberAccess(convToTarget, it);
                var convToObject = Expression.Convert(getPropValue, typeof(object));
                var lambda = Expression.Lambda(convToObject, arg);
                Func<object, object> expr = (Func<object, object>)lambda.Compile();
                result.Add(Tuple.Create(it.Name, expr));
            }

            return result;
        }
    }
}

特筆すべきはprivate static ConcurrentDictionary<Type, IEnumerable<Tuple<string, Func<object, object>>>> _cache = new ConcurrentDictionary<Type, IEnumerable<Tuple<string, Func<object, object>>>>();ですね。 140文字を超えているのでTwitterに書けません。

注目すべきはそこでは無いだろうと。 先ほどのPropertyInfoのキャッシュはファッション感覚でやってましたが、こちらのデリゲートの生成は凄まじくコストが高いので毎回走らせると死にます。 なのでちゃんとキャッシュしないとリフレクションよりも遅くなります。

気になる性能比

とりあえずテキトーに計測してみたところ式木の方が2倍ほど早かったです。 ちゃんと数値を取ったのでグラフにして可視化しようとも思ったのですが、しばらくぶりにgnuplotに触ったら使い方を完全に忘れていてなんか面倒になったのでやめました。 Excelでグラフを描くのを許されるのは小学生までですが、正直gnuplotも使い方を細かく覚えてないと面倒に感じます。

話がそれましたが、リフレクションを使うと桁が違うレベルで遅くなると予想していたので2倍程度の性能劣化ですむのはちょっと驚きました。

ちなみにConcurrentDictionaryを使っている理由は、実用するならスレッドセーフは必要な要件だと思ったからです。 普通のDictionarylockを使うとおそらくクリティカルセクションが大きくなりすぎて複数スレッドで呼び出すとロックが連発すると思います。

おわりに

やはり速度や生成される文字列の自由度の低さなどデメリットは多々ありますが、適当に突っ込むだけでわりといい感じにそれっぽいものを作ってくれるのでデバッグやログ目的に楽をしたいなら結構アリと考えています。

というより弊社の環境ではデバッグ目的にすでに使われています。ToStringの実装って面倒じゃないですか?

もっと早くしたいならReflectionでILをEmitしたりCallSiteしたりRoslynでコンパイルメタプログラミングに手を染めるなどの手もありますが、弊社にはそこまでの技術力はありませんでした。

おわり

ちなみに

割と便利なのでライブラリとして出来合いのものを作りました。

よかったら使ってみてください。

github.com