Github连结 : shps951023/ValueGetter
假如想了解过程的读者可以往下阅读。
缘起
自己做一个集合动态生成HtmlTableHelper专案
一开始使用Reflection GetValue的方法,版友程凯大反映这样效能不好,所以开始学习如何改为『缓存+动态生成Func换取速度』
,以下是学习过程
开始
假设有一个类别,我想要动态取得它的属性值
public class MyClass{public int MyProperty1 { get; set; } public int MyProperty2 { get; set; }}
一开始的直觉写法,使用反射(Reflection.GetValue)
var data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };var type = data.GetType();var props = type.GetProperties();var values = props.Select(s => s.GetValue(data)).ToList();
1.type.GetProperties()改为从cache抓取
前者Reflection方式,可以发现每一次呼叫方法都要重新从Reflection GetProperties()
造成重複抓取资料动作
所以改建立一个System.Type扩充方法,从Cache取得属性Info资料,并且使用ConcurrentDictionary达到线程安全。
public static partial class TypePropertyCacheHelper{private static readonly System.Collections.Concurrent.ConcurrentDictionary<RuntimeTypeHandle, IList<PropertyInfo>> TypeProperties = new System.Collections.Concurrent.ConcurrentDictionary<RuntimeTypeHandle, IList<PropertyInfo>>();public static IList<PropertyInfo> GetPropertiesFromCache(this Type type){if (TypeProperties.TryGetValue(type.TypeHandle, out IList<PropertyInfo> pis))return pis;return TypeProperties[type.TypeHandle] = type.GetProperties().ToList();}}
使用方式:
var data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };var type = data.GetType();var props = type.GetPropertiesFromCache();var values = props.Select(s => s.GetValue(data)).ToList();
2.PropertyInfo.GetValue()改为Expression动态建立Function + Cache
主要逻辑
使用方法参数类别反推泛型
使用Expression动态生成属性GetValue的方法,等Runctime Compile完后放进Cache生成的方法相当于 object GetterFunction(MyClass i) => i.MyProperty1 as object;
Cache unique key判断方式使用类别Handle + 属性MetadataToken,这边有个小细节呼叫ToString避免boxing动作
public static partial class ValueGetter{private static readonly ConcurrentDictionary<string, object> Functions = new ConcurrentDictionary<string, object>();public static object GetValueFromCache<T>(this PropertyInfo propertyInfo, T instance){if (instance == null) throw new ArgumentNullException($"{nameof(instance)} is null");if (propertyInfo == null) throw new ArgumentNullException($"{nameof(propertyInfo)} is null");var type = propertyInfo.DeclaringType;var key = $"{type.TypeHandle.Value.ToString()}|{propertyInfo.MetadataToken.ToString()}";Func<T, object> function = null;if (Functions.TryGetValue(key, out object func))function = func as Func<T, object>;else{function = CompileGetValueExpression<T>(propertyInfo);Functions[key] = function;}return function(instance);}public static Func<T, object> CompileGetValueExpression<T>(PropertyInfo propertyInfo){var instance = Expression.Parameter(propertyInfo.DeclaringType, "i");var property = Expression.Property(instance, propertyInfo);var convert = Expression.TypeAs(property, typeof(object));var lambda = Expression.Lambda<Func<T, object>>(convert, instance);return lambda.Compile();}}
但这时候会发现使用类型反推泛型
有一个问题,假如使用者传入向上转型
变数,代表CompileGetValueExpression<T>
方法的泛型T为object
这时候Compiler就会出现参数类型不符
错误,如图片
object data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" };
3.修正向上转型变数问题,使用GetValue()改为Emit动态建立强转型Function + Cache
主要逻辑
原本的function = func as Func<object, object>;
可以省掉转型动作生成方法相当于 object GetterFunction(object i) => ((MyClass)i).MyProperty1 as object ;
缺点在多了强转型动作
,但能避免参数类型不符问题(因为都是object型态)。public static partial class ValueGetter{private static readonly ConcurrentDictionary<object, Func<object, object>> Functions = new ConcurrentDictionary<object, Func<object, object>>();public static object GetObject<T>(this PropertyInfo propertyInfo, T instance){if (instance == null) return null;if (propertyInfo == null) throw new ArgumentNullException($"{nameof(propertyInfo)} is null");var key = $"{propertyInfo.DeclaringType.TypeHandle.Value.ToString()}|{propertyInfo.MetadataToken.ToString()}";if (Functions.TryGetValue(key, out Func<object, object> function))return function(instance);return (Functions[key] = GetFunction(propertyInfo))(instance);}public static Func<object, object> GetFunction(PropertyInfo prop){var type = prop.DeclaringType;var propGetMethod = prop.GetGetMethod(nonPublic: true);var propType = prop.PropertyType;var dynamicMethod = new DynamicMethod("m", typeof(object), new Type[] { typeof(object) }, type.Module);ILGenerator iLGenerator = dynamicMethod.GetILGenerator();LocalBuilder local0 = iLGenerator.DeclareLocal(propType);iLGenerator.Emit(OpCodes.Ldarg_0);iLGenerator.Emit(OpCodes.Castclass, type);iLGenerator.Emit(OpCodes.Call, propGetMethod);iLGenerator.Emit(OpCodes.Box, propType);iLGenerator.Emit(OpCodes.Ret);return (Func<object, object>)dynamicMethod.CreateDelegate(typeof(Func<object, object>));}}
使用方式:
public static IEnumerable<object> GetValues<T>(T instance){var type = instance.GetType();var props = type.GetPropertiesFromCache();return props.Select(s => s.GetObjectValue(instance));}
4.测试发现效率低问题
图片中CompilerFunction效率居然输给Reflection GetValue
关键点在从Cache取值动作太繁杂
所以写了最后一个版本,主要修改逻辑
取Cache动作分离到类别(ValueGetterCache),并藉由类别泛型来建立缓存字典
,Key取值不使用组合字串,直接使用int类型的PropertyInfo.MetadataToken
当缓存key值有些专案只需要返回字串型态,所以增加一个GetToStringValue方法,在生成的Func动作里加上ToString动作,这样可以少一次Boxing
动作做成扩充方法,方便使用using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.Linq;using System.Linq.Expressions;using System.Reflection;namespace ValueGetter{ public static partial class ValueGetter { /// <summary> /// Compiler Method Like: /// <code>string GetterFunction(object i) => (i as MyClass).MyProperty1.ToString() ; </code> /// </summary> public static Dictionary<string, string> GetToStringValues<T>(this T instance) => instance?.GetType().GetPropertiesFromCache().ToDictionary(key => key.Name, value => value.GetToStringValue<T>(instance)); /// <summary> /// Compiler Method Like: /// <code>string GetterFunction(object i) => (i as MyClass).MyProperty1.ToString() ; </code> /// </summary> public static string GetToStringValue<T>(this PropertyInfo propertyInfo, T instance) => instance != null ? ValueGetterCache<T, string>.GetOrAddToStringFuntionCache(propertyInfo)(instance) : null; } public static partial class ValueGetter { /// <summary> /// Compiler Method Like: /// <code>object GetterFunction(object i) => (i as MyClass).MyProperty1 as object ; </code> /// </summary> public static Dictionary<string, object> GetObjectValues<T>(this T instance) => instance?.GetType().GetPropertiesFromCache().ToDictionary(key => key.Name, value => value.GetObjectValue(instance)); /// <summary> /// Compiler Method Like: /// <code>object GetterFunction(object i) => (i as MyClass).MyProperty1 as object ; </code> /// </summary> public static object GetObjectValue<T>(this PropertyInfo propertyInfo, T instance) => instance!=null?ValueGetterCache<T, object>.GetOrAddFunctionCache(propertyInfo)(instance):null; } internal partial class ValueGetterCache<TParam, TReturn> { private static readonly ConcurrentDictionary<int, Func<TParam, TReturn>> ToStringFunctions = new ConcurrentDictionary<int, Func<TParam, TReturn>>(); private static readonly ConcurrentDictionary<int, Func<TParam, TReturn>> Functions = new ConcurrentDictionary<int, Func<TParam, TReturn>>(); } internal partial class ValueGetterCache<TParam, TReturn> { internal static Func<TParam, TReturn> GetOrAddFunctionCache(PropertyInfo propertyInfo) { var key = propertyInfo.MetadataToken; if (Functions.TryGetValue(key, out Func<TParam, TReturn> func)) return func; return (Functions[key] = GetCastObjectFunction(propertyInfo)); } private static Func<TParam, TReturn> GetCastObjectFunction(PropertyInfo prop) { var instance = Expression.Parameter(typeof(TReturn), "i"); var convert = Expression.TypeAs(instance, prop.DeclaringType); var property = Expression.Property(convert, prop); var cast = Expression.TypeAs(property, typeof(TReturn)); var lambda = Expression.Lambda<Func<TParam, TReturn>>(cast, instance); return lambda.Compile(); } } internal partial class ValueGetterCache<TParam, TReturn> { internal static Func<TParam, TReturn> GetOrAddToStringFuntionCache(PropertyInfo propertyInfo) { var key = propertyInfo.MetadataToken; if (ToStringFunctions.TryGetValue(key, out Func<TParam, TReturn> func)) return func; return (ToStringFunctions[key] = GetCastObjectAndToStringFunction(propertyInfo)); } private static Func<TParam, TReturn> GetCastObjectAndToStringFunction(PropertyInfo prop) { var propType = prop.PropertyType; var toStringMethod = propType.GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(p => p.Name == "ToString").First(); var instance = Expression.Parameter(typeof(TParam), "i"); var convert = Expression.TypeAs(instance, prop.DeclaringType); var property = Expression.Property(convert, prop); var tostring = Expression.Call(property, toStringMethod); var lambda = Expression.Lambda<Func<TParam, TReturn>>(tostring, instance); return lambda.Compile(); } } public static partial class PropertyCacheHelper { private static readonly Dictionary<RuntimeTypeHandle, IList<PropertyInfo>> TypePropertiesCache = new Dictionary<RuntimeTypeHandle, IList<PropertyInfo>>(); public static IList<PropertyInfo> GetPropertiesFromCache(this Type type) { if (TypePropertiesCache.TryGetValue(type.TypeHandle, out IList<PropertyInfo> pis)) return pis; return TypePropertiesCache[type.TypeHandle] = type.GetProperties().ToList(); } }}
使用方式:
var data = new MyClass() { MyProperty1 = 123, MyProperty2 = "test" }; var result = data.GetObjectValues(); //Result: Assert.AreEqual(123, result["MyProperty1"]); Assert.AreEqual("test", result["MyProperty2"]);
最后做一次效能测试
逻辑:
public class BenchmarkBase{ private static List<MyClass> Data = Enumerable.Range(1,100).Select(s=>new MyClass() { MyProperty1 = 123, MyProperty2 = "test" }).ToList(); [Benchmark()] public void Reflection() => Data.Select(instance => { var type = instance.GetType(); var props = type.GetProperties(); return props.ToDictionary(key => key.Name, value => value.GetValue(instance)); }).ToList(); [Benchmark()] public void ReflectionToString() => Data.Select(instance => { var type = instance.GetType(); var props = type.GetProperties(); return props.ToDictionary(key => key.Name, value => value.GetValue(instance).ToString()); }).ToList(); [Benchmark()] public void GetObjectValues() => Data.Select(s => s.GetObjectValues()).ToList(); [Benchmark()] public void GetObjectToStringValues() => Data.Select(s => s.GetToStringValues()).ToList();}
结果:
BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.648 (1803/April2018Update/Redstone4)Intel Core i7-7700 CPU 3.60GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0 ShortRun : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3362.0
结语
这边有做成一个NuGet套件ValueGetter,方便自己日后使用,也分享给有需要的版友
假如版友、前辈们有更好作法,期待留言、讨论。
最后过程中在S.O寻求很多帮助,有兴趣的读者可以在我S.O查看历史纪录。