谈谈对C#中反射的一些理解和认识(下)
在上一篇中我们列举了一些反射的常规的使用,这一篇我们将介绍一些关于关于反射的高级属性,这些包括创建对反射的性能的总结以及如何优化反射性能,以及通过InvokeMember的方法如何去调用反射等等,通过对这些内容的逐步熟悉,我们会对整个反射有一个更加深入的了解与认识,在文章的最后我们会附上一个小DEMO从而供以后查阅使用,通过对比几种不同的方法,我们来分别看各种方式进行操作的性能的差异,从而方便后续做出正确的选择,在下面的例子中我们将比较:直接访问花费的时间、采用EmitSet花费的时间、纯反射花费的时间、采用泛型委托花费的时间以及采用通用接口花费的时间,从而就这几种方式来做一组对比。
首先我们需要封装一个Model从而供后面的代码进行调用
public class OrderInfo { public int OrderID { get; set; } public DateTime OrderDate { get; set; } public decimal SumMoney { get; set; } public string Comment { get; set; } public bool Finished { get; set; } public int Add(int a, int b) { return a + b; } }
1 首先是设置属性时进行的对比
在进行最终的数据对比之前我们来看看这里面涉及到的几种调用方式分别表示什么?
A 直接访问花费时间
这个非常容易理解,就是直接new一个OrderInfo对象,然后对立面的各种属性进行读写操作,典型的面向对象的写法,这个是很好理解的,这里就不再赘述。
B EmitSet花费时间
这里需要看我们设置OrderInfo的属性值的时候是通过下面的这句代码来完成的:
SetValueDelegate setter2 = DynamicMethodFactory.CreatePropertySetter(propInfo);
这里SetValueDelegate是一个委托,后面的工厂方法用于创建这个委托
public static SetValueDelegate CreatePropertySetter(PropertyInfo property) { if( property == null ) { throw new ArgumentNullException("property"); } if( !property.CanWrite ) { return null; } MethodInfo setMethod = property.GetSetMethod(true); DynamicMethod dm = new DynamicMethod("PropertySetter", null, new Type[] { typeof(object), typeof(object) }, property.DeclaringType, true); ILGenerator il = dm.GetILGenerator(); if( !setMethod.IsStatic ) { il.Emit(OpCodes.Ldarg_0); } il.Emit(OpCodes.Ldarg_1); EmitCastToReference(il, property.PropertyType); if( !setMethod.IsStatic && !property.DeclaringType.IsValueType ) { il.EmitCall(OpCodes.Callvirt, setMethod, null); } else { il.EmitCall(OpCodes.Call, setMethod, null); } il.Emit(OpCodes.Ret); return (SetValueDelegate)dm.CreateDelegate(typeof(SetValueDelegate)); }
这段代码确实不太容易理解,我们取其中的关键的部分来进行解释
DynamicMethod:定义并表示一种可编译、执行和丢弃的动态方法。丢弃的方法可用于垃圾回收。具体的实例请参考这里。
dm.GetILGenerator():为该方法返回一个具有默认 MSIL 流大小(64 字节)的 Microsoft 中间语言 (MSIL) 生成器。
il.Emit:将指定的指令放到指令流上。
il.EmitCall:将 call 或 callvirt 指令放到 Microsoft 中间语言 (MSIL) 流上,以便调用 varargs 方法。关于Call和Callvirt的区别,参考其定义:
Call:调用由传递的方法说明符指示的方法。
Callvirt:对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
在调用这些方法后,完成动态方法并创建一个可用于执行该方法的委托,通过这些委托来完成对OrderInfo这个对象的属性的赋值的过程。
C 纯反射花费的时间
这项就更好理解了,首先通过PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID");来获取到PropertyInfo对象,然后调用propInfo.SetValue(testObj, 123, null);来直接进行反射赋值,当然这种方法在进行大量的赋值时性能较差,这个可以通过后面的比较来进行说明。
D 泛型委托花费的时间
这种方法通过定义一个泛型类SetterWrapper<TTarget, TValue>,然后进行实例化,最后调用SetValue进行赋值。在SetterWrapper的构造函数中通过Delegate.CreateDelegate创建一个委托,委托对应的方法是MethodInfo m = propertyInfo.GetSetMethod(true);这种方法由于在实例化时确定了TTarget, TValue的类型,所以在最终的赋值的时候就没有了类型转换时的装箱和拆箱的操作,所以相对纯反射来说更快,但是相对于Emit这种直接操作MSIL方法来说,效果可能要差一些。
public class SetterWrapper<TTarget, TValue> : ISetValue { private Action<TTarget, TValue> _setter; public SetterWrapper(PropertyInfo propertyInfo) { if( propertyInfo == null ) throw new ArgumentNullException("propertyInfo"); if( propertyInfo.CanWrite == false ) throw new NotSupportedException("属性不支持写操作。"); MethodInfo m = propertyInfo.GetSetMethod(true); _setter = (Action<TTarget, TValue>)Delegate.CreateDelegate(typeof(Action<TTarget, TValue>), null, m); } public void SetValue(TTarget target, TValue val) { _setter(target, val); } void ISetValue.Set(object target, object val) { _setter((TTarget)target, (TValue)val); } }
E 通用接口花费时间
采用通用接口这种方式,能在一定程度上简化调用的方式,但缺点也是非常明显的,首先我们来看看接口中采用的定义:
public interface ISetValue { void Set(object target, object val); }
由于接口中定义的Set方法都是采用的Object作为参数,这在通用性上确实比较好,但是在由于传入的参数不可能都是Object类型,所以当传入值类型时就需要进行类型转换,进行拆箱操作,所以在进行大量操作时不可避免会有性能的损失,这个可以通过后面代码运行的时间来判断其优劣。
另外由于ISetValue这一接口是在泛型类SetterWrapper<TTarget, TValue>中继承并实现的,所以在创建泛型类实例的时候,需要定义泛型类型,最终的实现是通过下面的代码来实现的
Type instanceType = typeof(SetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType); return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
public static ISetValue CreatePropertySetterWrapper(PropertyInfo propertyInfo) { if(propertyInfo == null) { throw new ArgumentNullException("propertyInfo"); } if(propertyInfo.CanWrite == false) { throw new NotSupportedException("属性不支持写操作。"); } MethodInfo mi = propertyInfo.GetSetMethod(true); if( mi.GetParameters().Length >1) { throw new NotSupportedException("不支持构造索引器属性的委托。"); } if( mi.IsStatic ) { Type instanceType = typeof(StaticSetterWrapper<>).MakeGenericType(propertyInfo.PropertyType); return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo); } else { Type instanceType = typeof(SetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType); return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo); } }
F:FastSet花费时间
这个实现方式在本质上是和通用接口实现方式一模一样的,唯一不同的是第一次创建实例后会缓存在一个Hashtable中,这样下次再获取当前实例的时候多了一个遍历Hashtable的过程了,所以最终会有一定的时间花费,其它的和通用接口的实现方法无异。
G:FastSet2花费时间
这个部分和FastSet比较起来的差别就是最终采用的是EmitSet的方式来实现的赋值过程,所以没有类型转换而且最终赋值的过程是直接对MSIL进行操作的,所以最后的时间也是比较短的。
在介绍完这些内容后,我们通过下面的几个方法来进行10万次的重复实验过程,从而比较他们之间的差别,从而对整体有个直观的比较。
static void Test_SetProperty(int count) { OrderInfo testObj = new OrderInfo(); PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID"); Console.Write("直接访问花费时间: "); Stopwatch watch1 = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) { testObj.OrderID = 123; } watch1.Stop(); Console.WriteLine(watch1.Elapsed.ToString()); SetValueDelegate setter2 = DynamicMethodFactory.CreatePropertySetter(propInfo); Console.Write("EmitSet花费时间: "); Stopwatch watch2 = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) { setter2(testObj, 123); } watch2.Stop(); Console.WriteLine(watch2.Elapsed.ToString()); Console.Write("纯反射花费时间: "); Stopwatch watch3 = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) { propInfo.SetValue(testObj, 123, null); } watch3.Stop(); Console.WriteLine(watch3.Elapsed.ToString()); Console.Write("泛型委托花费时间: "); SetterWrapper<OrderInfo, int> setter3 = new SetterWrapper<OrderInfo, int>(propInfo); Stopwatch watch4 = Stopwatch.StartNew(); for(int i = 0; i < count; i++) { setter3.SetValue(testObj, 123); } watch4.Stop(); Console.WriteLine(watch4.Elapsed.ToString()); Console.Write("通用接口花费时间: "); ISetValue setter4 = GetterSetterFactory.CreatePropertySetterWrapper(propInfo); Stopwatch watch5 = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) { setter4.Set(testObj, 123); } watch5.Stop(); Console.WriteLine(watch5.Elapsed.ToString()); propInfo.FastSetValue(testObj, 123); Console.Write("FastSet花费时间: "); Stopwatch watch6 = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) propInfo.FastSetValue(testObj, 123); watch6.Stop(); Console.WriteLine(watch6.Elapsed.ToString()); propInfo.FastSetValue2(testObj, 123); Console.Write("FastSet2花费时间: "); Stopwatch watch6b = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) propInfo.FastSetValue2(testObj, 123); watch6b.Stop(); Console.WriteLine(watch6b.Elapsed.ToString()); Hashtable table = new Hashtable(); table[propInfo] = new object(); Console.Write("Hashtable花费时间: "); Stopwatch watch7 = Stopwatch.StartNew(); for( int i = 0; i < count; i++ ) { object val = table[propInfo]; } watch7.Stop(); Console.WriteLine(watch7.Elapsed.ToString()); Console.WriteLine("-------------------"); Console.WriteLine("纯反射花费时间({0}) / 直接访问花费时间({1}) = {2}", watch3.Elapsed.ToString(), watch1.Elapsed.ToString(), watch3.Elapsed.TotalMilliseconds / watch1.Elapsed.TotalMilliseconds); Console.WriteLine("纯反射花费时间({0})/ 通用接口花费时间({1}) = {2}", watch3.Elapsed.ToString(), watch5.Elapsed.ToString(), watch3.Elapsed.TotalMilliseconds / watch5.Elapsed.TotalMilliseconds); Console.WriteLine("纯反射花费时间({0}) / FastSet花费时间({1}) = {2}", watch3.Elapsed.ToString(), watch6.Elapsed.ToString(), watch3.Elapsed.TotalMilliseconds / watch6.Elapsed.TotalMilliseconds);
}
通过下面的截图,我们能够清晰看出每种访问方式之间的差异,了解了这些差异后我们能够在进行反射时采用一种最合理的方式来使用,同时我们也需要加深对Emit这种技术的理解,从而在改善软件性能方面有自己的方法。
上面的示例仅仅是对OrderInfo的某一个属性进行赋值的操作时所作出的对比,在读取和创建类型实例的时候结果也是相同的,今天就介绍到这里,最后分享整个软件的DEMO,如果有需要可以点击进行下载。