前期綁定與后期綁定 在.NET中,前期綁定(Early Binding)是指在編譯時就確定了對象的類型和方法,而后期綁定(Late Binding)或動態(tài)綁定是在運(yùn)行時確定對象的類型和方法。
前置知識:C#類型系統(tǒng)結(jié)構(gòu) C#作為C++++ ,在類型系統(tǒng)上沿用C++的類型系統(tǒng)
前期綁定 在代碼能執(zhí)行之前,將代碼中依賴的assembly,module,class,method,field等類型系統(tǒng) 的元素提前構(gòu)建好。 前期綁定的優(yōu)點(diǎn)是編譯時類型檢查,提高了類型安全性和性能。缺點(diǎn)是如果需要更換類型,需要重新編譯代碼。靈活性不夠
比如一個簡單的的控制臺,就自動提前加載了各種需要的DLL文件。完成前期綁定。
后期綁定 后期綁定的優(yōu)點(diǎn)是可以在運(yùn)行時更改類型,無需重新編譯代碼。缺點(diǎn)是在編譯時不進(jìn)行類型檢查,可能導(dǎo)致運(yùn)行時錯誤。 幾個常用的場景,比如dynamic ,多態(tài),System.Reflection 等
舉個例子,使用Reflection下的“元數(shù)據(jù)查詢API”,動態(tài)加載DLL
var dllpath = "xxx.dll" ;
Assembly assembly = Assembly.LoadFrom(dllpath);
Type dataAccessType = assembly.GetType("xxxxx" );
object dataAccess = Activator.CreateInstance(dataAccessType);
MethodInfo addMethod = dataAccessType.GetMethod("Add" );
addMethod.Invoke(dataAccess, new object [] { "hello world" });
反射 反射的本質(zhì)就是“操作元數(shù)據(jù)”
什么是元數(shù)據(jù)? MetaData,本是上就是存儲在dll中的一個信息數(shù)據(jù)庫,記錄了這個assembled中有哪些方法,哪些類,哪些屬性等等信息 可以看到,各種Table組成的信息,是不是類似一個數(shù)據(jù)庫?
舉個例子: 執(zhí)行Type.GetType("int"),反射會在MetaData尋找"int"的類型。但在運(yùn)行時會返回null.因?yàn)镸etaData中只有"System.Int32"這個字符串。
通過Reflection XXXInfo系列API 查詢所有細(xì)節(jié)
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 );
......
反射如何構(gòu)建類型系統(tǒng) 通過Reflection XXXBuilder系列API 構(gòu)建一個全新的類型
AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName ("MyAssembly" ), AssemblyBuilderAccess.RunAndCollect);
ModuleBuilder mob = ab.DefineDynamicModule("MyModule" );
TypeBuilder tb = mob.DefineType("MyType" , TypeAttributes.Public | TypeAttributes.Class);
MethodBuilder mb = tb.DefineMethod("SumMethod" , MethodAttributes.Public | MethodAttributes.Static, typeof(int ), new Type [] { typeof(int ), typeof(int ) });
ILGenerator il = mb.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
Type type = tb.CreateType();
MethodInfo method = type.GetMethod("SumMethod" );
Console.WriteLine(method.Invoke(null , new object [] { 5 , 10 }));
反射底層調(diào)用 C#的類型系統(tǒng),與C++的類型系統(tǒng)是一一對應(yīng)的。因此其底層必定是調(diào)用C++的方法。 示意圖如下,有興趣的小伙伴可以去參考coreclr的源碼
眼見為實(shí),以Invoke為例
反射到底慢在哪? 動態(tài)解析 從上面可知道,反射作為后期綁定,在runtime中要根據(jù)metadata查詢出信息,嚴(yán)重依賴字符串匹配,這本身就增加了額外的操作 動態(tài)調(diào)用 使用反射調(diào)用方法時,先要將參數(shù)打包成數(shù)組,再解包到線程棧上。又是額外操作。 無法在編譯時優(yōu)化 反射是動態(tài)的臨時調(diào)用,JIT無法優(yōu)化。只能根據(jù)代碼一步一步執(zhí)行。 額外的安全檢查 反射會涉及到訪問和修改只讀字段等操作,運(yùn)行時需要進(jìn)行額外的安全性檢查,這也會增加一定的開銷 緩存易失效 反射如果參數(shù)發(fā)生變化,那么緩存的匯編就會失效。又需要重新查找與解析。 總之,千言萬語匯成一句話。最好的反射就是不要用反射。除非你能保證對性能要求不高/緩存高命中率
CLR的對反射的優(yōu)化 除了緩存反射的匯編,.NET 中提供了一系列新特性來盡可能的繞開“反射”
Emit Emit 是 .NET 提供的一種動態(tài)生成和編譯代碼的技術(shù)。通過 Emit,我們可以動態(tài)生成一個新的方法,這個方法可以直接訪問私有成員,這對于一些特殊場景非常有用,比如動態(tài)代理、代碼生成器、AOP(面向切面編程)等.
public class Person
{
private int _age;
public override string ToString ()
{
return _age.ToString();
}
}
static void EmitTest (Person person )
{
Type personType = typeof (Person);
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);
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
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 );
}
切構(gòu)建代碼又臭又長。
Expression Expression 是 .NET 提供的一種表達(dá)式樹的技術(shù)。通過 Expression,我們可以創(chuàng)建一個表達(dá)式樹,然后編譯這個表達(dá)式樹,生成一個可以訪問私有成員的方法
static void ExpressionTest (Person person)
{
Type personType = typeof(Person);
FieldInfo ageField = personType.GetField("_age" , BindingFlags.Instance | BindingFlags.NonPublic);
if (ageField == null )
{
throw new ArgumentException ("未找到指定的私有字段" );
}
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 );
}
切構(gòu)建代碼又臭又長。
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語言的定義。它定義了什么是私有類型,它的行為是什么。 但對于程序本身來說,代碼和數(shù)據(jù)都只是一段內(nèi)存,實(shí)際上你的指針想訪問哪就訪問哪。哪管你什么私有類型。換一個指向地址不就得了。因此CLR開放了這么一個口子,利用外部訪問直接操作內(nèi)存。看它的命名Unsafe Accessor就能猜到意圖了
3,2,1. 上匯編?。?! 直接將rax寄存器偏移量+8,直接返回int(占用4字節(jié),偏移量8)類型的_age。 沒有Emit,Expression的彎彎繞繞,絲毫不拖泥帶水。
.NET 9中的改進(jìn) 支持泛型,更優(yōu)雅。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
轉(zhuǎn)自https://www.cnblogs.com/lmy5215006/p/18545334
該文章在 2024/11/16 8:18:06 編輯過