简介
反射,反射,程序员的快乐。
前期绑定与后期绑定
在.NET中,前期绑定(Early Binding)是指在编译时就确定了对象的类型和方法,而后期绑定(Late Binding)或动态绑定是在运行时确定对象的类型和方法。
前置知识:C#类型系统结构
C#作为C++++ ,在类型系统上沿用C++的类型系统
前期绑定
在代码能执行之前,将代码中依赖的assembly,module,class,method,field等类型系统的元素提前构建好。
前期绑定的优点是编译时类型检查,提高了类型安全性和性能。缺点是如果需要更换类型,需要重新编译代码。灵活性不够
比如一个简单的的控制台,就自动提前加载了各种需要的DLL文件。完成前期绑定。
后期绑定
后期绑定的优点是可以在运行时更改类型,无需重新编译代码。缺点是在编译时不进行类型检查,可能导致运行时错误。
几个常用的场景,比如dynamic ,多态,System.Reflection等
举个例子,使用Reflection下的“元数据查询API”,动态加载DLL
1 2 3 4 5 6 | var dllpath = "xxx.dll" ; Assembly assembly = Assembly.LoadFrom(dllpath); //构建Assembly+Module Type dataAccessType = assembly.GetType( "xxxxx" ); //构建Class(MethodTable+EEClass) object dataAccess = Activator.CreateInstance(dataAccessType); //在托管堆中创建MT实例 MethodInfo addMethod = dataAccessType.GetMethod( "Add" ); //构建MethodDesc addMethod.Invoke(dataAccess, new object [] { "hello world" }); //调用方法 |
反射
反射的本质就是“操作元数据”
什么是元数据?
MetaData,本是上就是存储在dll中的一个信息数据库,记录了这个assembled中有哪些方法,哪些类,哪些属性等等信息
可以看到,各种Table组成的信息,是不是类似一个数据库?
举个例子:
执行Type.GetType(“int”),反射会在MetaData寻找”int”的类型。但在运行时会返回null.因为MetaData中只有”System.Int32″这个字符串。
反射如何查询MetaData?
通过Reflection XXXInfo系列API 查询所有细节
1 2 3 4 5 | Type t = typeof (System.IO.FileStream); FieldInfo[] fi = t.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); PropertyInfo[] pi = t.GetProperties(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); EventInfo[] ei = t.GetEvents(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); ...... |
反射如何构建类型系统
通过Reflection XXXBuilder系列API 构建一个全新的类型
1 2 3 4 5 6 7 8 9 10 11 12 | AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName( "MyAssembly" ), AssemblyBuilderAccess.RunAndCollect); //创建Assembly ModuleBuilder mob = ab.DefineDynamicModule( "MyModule" ); //创建Module TypeBuilder tb = mob.DefineType( "MyType" , TypeAttributes.Public | TypeAttributes.Class); //创建Class MethodBuilder mb = tb.DefineMethod( "SumMethod" , MethodAttributes.Public | MethodAttributes.Static, typeof ( int ), new Type[] { typeof ( int ), typeof ( int ) }); //创建MethodTable ILGenerator il = mb.GetILGenerator(); //通过IL API 动态构建MethodDesc il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Add); il.Emit(OpCodes.Ret); Type type = tb.CreateType(); //mt + eeclass MethodInfo method = type.GetMethod( "SumMethod" ); Console.WriteLine(method.Invoke( null , new object [] { 5, 10 })); |
反射底层调用
C#的类型系统,与C++的类型系统是一一对应的。因此其底层必定是调用C++的方法。
示意图如下,有兴趣的小伙伴可以去参考coreclr的源码
眼见为实,以Invoke为例
反射到底慢在哪?
- 动态解析
从上面可知道,反射作为后期绑定,在runtime中要根据metadata查询出信息,严重依赖字符串匹配,这本身就增加了额外的操作 - 动态调用
使用反射调用方法时,先要将参数打包成数组,再解包到线程栈上。又是额外操作。 - 无法在编译时优化
反射是动态的临时调用,JIT无法优化。只能根据代码一步一步执行。 - 额外的安全检查
反射会涉及到访问和修改只读字段等操作,运行时需要进行额外的安全性检查,这也会增加一定的开销 - 缓存易失效
反射如果参数发生变化,那么缓存的汇编就会失效。又需要重新查找与解析。
总之,千言万语汇成一句话。最好的反射就是不要用反射。除非你能保证对性能要求不高/缓存高命中率
CLR的对反射的优化
除了缓存反射的汇编,.NET 中提供了一系列新特性来尽可能的绕开“反射”
Emit
Emit 是 .NET 提供的一种动态生成和编译代码的技术。通过 Emit,我们可以动态生成一个新的方法,这个方法可以直接访问私有成员,这对于一些特殊场景非常有用,比如动态代理、代码生成器、AOP(面向切面编程)等.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class Person { private int _age; public override string ToString() { return _age.ToString(); } } static void EmitTest(Person person) { // 获取Person类的类型对象 Type personType = typeof (Person); // 获取私有字段_age的FieldInfo,无法避免部分使用反射 FieldInfo ageField = personType.GetField( "_age" , BindingFlags.Instance | BindingFlags.NonPublic); if (ageField == null ) { throw new ArgumentException( "未找到指定的私有字段" ); } // 创建一个动态方法 DynamicMethod dynamicMethod = new DynamicMethod( "SetAgeValue" , null , new Type[] { typeof (Person), typeof ( int ) }, personType); // 获取IL生成器 ILGenerator ilGenerator = dynamicMethod.GetILGenerator(); // 将传入的Person对象加载到计算栈上(this指针) ilGenerator.Emit(OpCodes.Ldarg_0); // 将传入的新值加载到计算栈上 ilGenerator.Emit(OpCodes.Ldarg_1); // 将新值存储到对应的私有字段中 ilGenerator.Emit(OpCodes.Stfld, ageField); // 返回(因为方法无返回值,这里只是结束方法执行) ilGenerator.Emit(OpCodes.Ret); // 创建委托类型,其签名与动态方法匹配 Action setAgeAction = (Action)dynamicMethod.CreateDelegate( typeof (Action)); // 通过委托调用动态生成的方法来修改私有字段的值 setAgeAction(person, 100); } |
切构建代码又臭又长。
Expression
Expression 是 .NET 提供的一种表达式树的技术。通过 Expression,我们可以创建一个表达式树,然后编译这个表达式树,生成一个可以访问私有成员的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static void ExpressionTest(Person person) { // 获取Person类的类型对象 Type personType = typeof (Person); // 获取私有字段_age的FieldInfo,无法避免部分使用反射 FieldInfo ageField = personType.GetField( "_age" , BindingFlags.Instance | BindingFlags.NonPublic); if (ageField == null ) { throw new ArgumentException( "未找到指定的私有字段" ); } // 创建参数表达式,对应传入的Person对象实例 ParameterExpression instanceParam = Expression.Parameter(personType, "instance" ); // 创建参数表达式,对应传入的新值 ParameterExpression newValueParam = Expression.Parameter( typeof ( int ), "newValue" ); // 创建一个赋值表达式,将新值赋给私有字段 BinaryExpression assignExpression = Expression.Assign(Expression.Field(instanceParam, ageField), newValueParam); // 创建一个包含赋值表达式的表达式块,这里因为只有一个赋值操作,所以块里就这一个表达式 BlockExpression blockExpression = Expression.Block(assignExpression); // 创建一个可执行的委托,其类型与表达式块的逻辑匹配 Action setAgeAction = Expression.Lambda>(blockExpression, instanceParam, newValueParam).Compile(); // 通过委托调用表达式树生成的逻辑来修改私有字段的值 setAgeAction(person, 100); } |
切构建代码又臭又长。
UnsafeAccessorAttribute
.Net 8中引入了新特性UnsafeAccessorAttribute 。
使用该特性,来提供对私有字段的快速修改
1 2 3 4 5 6 7 | static void New() { var person = new Person(); GetAgeField(person) = 100; } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_age" )] static extern ref int GetAgeField(Person counter); |
为什么它这么快?
对于C#来说,私有类型是OOP语言的定义。它定义了什么是私有类型,它的行为是什么。
但对于程序本身来说,代码和数据都只是一段内存,实际上你的指针想访问哪就访问哪。哪管你什么私有类型。换一个指向地址不就得了。因此CLR开放了这么一个口子,利用外部访问直接操作内存。看它的命名UnsafeAccessor就能猜到意图了
3,2,1. 上汇编!!!
直接将rax寄存器偏移量+8,直接返回int(占用4字节,偏移量8)类型的_age。 没有Emit,Expression的弯弯绕绕,丝毫不拖泥带水。
.NET 9中的改进
参考资料
到此这篇关于.NET Core 反射底层原理浅谈的文章就介绍到这了,更多相关.NET Core 反射底层原理内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!