简介

反射,反射,程序员的快乐。

前期绑定与后期绑定

在.NET中,前期绑定(Early Binding)是指在编译时就确定了对象的类型和方法,而后期绑定(Late Binding)或动态绑定是在运行时确定对象的类型和方法。

前置知识:C#类型系统结构

C#作为C++++ ,在类型系统上沿用C++的类型系统

前期绑定

在代码能执行之前,将代码中依赖的assembly,module,class,method,field等类型系统的元素提前构建好。

前期绑定的优点是编译时类型检查,提高了类型安全性和性能。缺点是如果需要更换类型,需要重新编译代码。灵活性不够

比如一个简单的的控制台,就自动提前加载了各种需要的DLL文件。完成前期绑定。

后期绑定

后期绑定的优点是可以在运行时更改类型,无需重新编译代码。缺点是在编译时不进行类型检查,可能导致运行时错误。

几个常用的场景,比如dynamic ,多态,System.Reflection等

举个例子,使用Reflection下的“元数据查询API”,动态加载DLL

            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 查询所有细节

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 构建一个全新的类型

            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为例

反射到底慢在哪?

  1. 动态解析

    从上面可知道,反射作为后期绑定,在runtime中要根据metadata查询出信息,严重依赖字符串匹配,这本身就增加了额外的操作
  2. 动态调用

    使用反射调用方法时,先要将参数打包成数组,再解包到线程栈上。又是额外操作。
  3. 无法在编译时优化

    反射是动态的临时调用,JIT无法优化。只能根据代码一步一步执行。
  4. 额外的安全检查

    反射会涉及到访问和修改只读字段等操作,运行时需要进行额外的安全性检查,这也会增加一定的开销
  5. 缓存易失效

    反射如果参数发生变化,那么缓存的汇编就会失效。又需要重新查找与解析。

总之,千言万语汇成一句话。最好的反射就是不要用反射。除非你能保证对性能要求不高/缓存高命中率

CLR的对反射的优化

除了缓存反射的汇编,.NET 中提供了一系列新特性来尽可能的绕开“反射”

Emit

Emit 是 .NET 提供的一种动态生成和编译代码的技术。通过 Emit,我们可以动态生成一个新的方法,这个方法可以直接访问私有成员,这对于一些特殊场景非常有用,比如动态代理、代码生成器、AOP(面向切面编程)等.

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<Person, int> setAgeAction = (Action<Person, int>)dynamicMethod.CreateDelegate(typeof(Action<Person, int>)); // 通过委托调用动态生成的方法来修改私有字段的值
setAgeAction(person, 100);
}

切构建代码又臭又长。

Expression

Expression 是 .NET 提供的一种表达式树的技术。通过 Expression,我们可以创建一个表达式树,然后编译这个表达式树,生成一个可以访问私有成员的方法

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<Person, int> setAgeAction = Expression.Lambda<Action<Person, int>>(blockExpression, instanceParam, newValueParam).Compile(); // 通过委托调用表达式树生成的逻辑来修改私有字段的值
setAgeAction(person, 100);
}

切构建代码又臭又长。

UnsafeAccessorAttribute

.Net 8中引入了新特性UnsafeAccessorAttribute 。

使用该特性,来提供对私有字段的快速修改

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中的改进

支持泛型,更优雅。

https://learn.microsoft.com/zh-cn/dotnet/core/compatibility/core-libraries/9.0/unsafeaccessor-generics

参考资料

https://blog.csdn.net/sD7O95O/article/details/133002995

https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute?view=net-8.0

.NET Core 反射底层原理浅谈的更多相关文章

  1. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  2. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  3. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  4. JAVA CAS原理浅谈

    java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包.可见CAS的重要性. CAS CAS:Compare and Swap, 翻译成比较并交换. java.uti ...

  5. Java中的SPI原理浅谈

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...

  6. CAS+SSO原理浅谈

    http://www.cnblogs.com/yonsin/archive/2009/08/29/1556423.htmlSSO 是一个非常大的主题,我对这个主题有着深深的感受,自从广州 UserGr ...

  7. php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  8. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  9. Docker 基础底层架构浅谈

    docker学习过程中,免不了需要学习下docker的底层技术,今天我们来记录下docker的底层架构吧! 从上图我们可以看到,docker依赖于linux内核的三个基本技术:namespaces.C ...

  10. JDK source 之 LinkedHashMap原理浅谈

    注:本文参考JDK1.7.0_45源码. LinkedHashMap是基于HashMap实现的数据结构,与HashMap主要的不同为每个Entry是使用双向链表实现的,并且提供了根据访问顺序进行排序的 ...

随机推荐

  1. JavaScript设计模式样例十三 —— 模版方法模式

    模板方法模式(Template Method Pattern) 定义:一个抽象类公开定义了执行它的方法的方式/模板.它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行.目的:一些方法通用 ...

  2. Swahili-text:华中大推出非洲语言场景文本检测和识别数据集 | ICDAR 2024

    论文提出了一个专门针对斯瓦希里语自然场景文本检测和识别的数据集,这在当前研究中是一个未充分开发的语言领域.数据集包括976张带标注的场景图像,可用于文本检测,以及8284张裁剪后的图像用于识别. 来源 ...

  3. MSYS2、MinGW、Cygwin 关系梳理

    还记得大一刚开始写 C 代码时,经常看到 MSYS2.MinGW.Cygwin 等名词.对于第一次接触编程的我来说这些名词让我眼花缭乱.当时查阅了一些资料,但是对于这些名词的解释始终让我云里雾里.现在 ...

  4. 详解JVM 内存结构与实战调优总结

    详解JVM 内存结构与实战调优总结 GC优化案例做个总结: 1在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间.我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过GC优化 ...

  5. 终于有人把Modbus讲明白了

    大家好!我是付工. 2012年开始接触Modbus协议,至今已经有10多年了,从开始的懵懂,到后来的顿悟,再到现在的开悟,它始终岿然不动,变化的是我对它的认知和理解. 今天跟大家聊聊关于Modbus协 ...

  6. OIDC – OpenIddict Core

    3 选 1 IdentityServer 4 本来 IdentityServer 4 一直都是首选的, 但在 2020 年他们决定成立公司, IdentityServer 5 就开始收费了. The ...

  7. 图解YUV420、YUV420(YUY2)、YUV422(YVYU)

    Y:亮度分量    UV:色度分量 1.  标准yuv指的是yuv420 一般看文章,会出现下面的公式,但不涉及具体的yuv格式,其实这一定指的是yuv420 Y与RGB的演算关系为:Y = 0.21 ...

  8. flops, params = profile(model, inputs=(x,))计算

    计算量:FLOPs,FLOP时指浮点运算次数,s是指秒,即每秒浮点运算次数的意思,考量一个网络模型的计算量的标准.参数量:Params,是指网络模型中需要训练的参数总数. flops(G) = flo ...

  9. nestjs 登录和验证码结合验证 svgCaptcha 包 session 会话标识

    // ps: 现在用户验证使用 token jwt 了 代替了 session // session 是服务器为每个用户建立的唯一标识 以区分用户 会话标识 // session 是express中的 ...

  10. jQuery的$(document).ready(function(){}) 和 原生 js 的load 等待加载事件有什么不同

    jQuery 的 $(function (){})  函数入口需要等待 DOM 结构绘制完成才会执行 , 不用等待外部资源加载完毕  和原生js 的 DOMContentLoaded 类似 , 2 者 ...