【C#】分享开源小工具 ValueGetter,轻量快速取物件值

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" };

20190322125948-image.png

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取值动作太繁杂
20190323170857-image.png

所以写了最后一个版本,主要修改逻辑

取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
MethodMeanGen 0AllocatedGetObjectValues32.93 us9.875040.51 KBGetObjectToStringValues38.23 us10.062541.29 KBReflection54.40 us10.062541.29 KBReflectionToString60.24 us10.812544.42 KB

结语

这边有做成一个NuGet套件ValueGetter,方便自己日后使用,也分享给有需要的版友
假如版友、前辈们有更好作法,期待留言、讨论。

最后过程中在S.O寻求很多帮助,有兴趣的读者可以在我S.O查看历史纪录。


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章