1. 程式人生 > >反射(reflection和reflection.emit)

反射(reflection和reflection.emit)

    反射分為兩部分:

1. 發現執行中物件的資訊,呼叫其中的方法、屬性值等,用到的是system.reflection名稱空間下的類中的方法;

2. 在執行時動態產生程式碼,用到的是system.reflection.emit名稱空間下的類中的方法。

第一部分:

首先,你必須在一個已知的型別或程式集上建立類Reflection.Assembly的一個例項,可以使用類Assembly的靜態的方法:Load來實現的。該方法有很多版本,我們可以舉兩個例子:

  • Public Static Assembly Load(string):引數為程式集的名稱,即名稱空間,比如:Assembly myAssembly 
    = Assembly.Load("System.Drawing");
  • Public Static Assembly LoadFrom(strig):引數為程式集的全路徑,比如:Assembly myAssembly = Assembly.LoadFrom(@"C:\WINNT\Microsoft.NET\Framework\v1.1.4322\System.Drawing.dll");

也可以通過下面幾種方法獲取Assembly裡的例項:

  • Assembly myAssembly =typeof(System.Data.DataRow).Assembly;
  • DataTable dt =new DataTable();    Assembly myAssembly 
    = dt.GetType().Assembly;

接下來該介紹如何獲取程式集內容的一些資訊,比如類、方法、屬性、欄位等。

可以通過Type[] types = myAssembly.GetTypes();來獲取型別陣列,然後遍歷type來獲取其中的資訊,比如:

Type[] types = myAssembly.GetTypes();

foreach(Type type in types)

{

    if(type.IsClass)

        {

            Console.WriteLine(type.Name);

        }

}

然後,獲取到程式集中的型別後,我們可以對其中的型別進行篩選(搜尋、過濾和查詢)。

定義如下的一個類:

publicclass SomeClass
{

privateint myPrvField1 =15;

privatestring myPrvField2 ="Some private field";

publicdecimal myPubField1 =1.03m;
 
public SomeClass()
{}public SomeClass(int someValue)
{}public SomeClass(int someValue,int someOtherValue)
{}publicvoid SomeMethod()
{}

public staticvoid OtherMethod()
{}

publicstaticvoid AnotherMethod()
{}

}

        1.  可以通過一些方法直接獲取程式集中的建構函式(GetConstructor)、方法(GetMethod)、屬性(GetProperty)和事件(GetEvent),比如:

  • Type[] ts = {typeof(Int32)}; ConstructorInfo ci =typeof(SomeClass).GetConstructor(ts);  該方法可以獲取到帶有一個int型別的引數的建構函式資訊例項
  • MethodInfo mi =typeof(SomeClass).GetMethod("SomeMethod");  把方法名作為引數傳遞到該方法中,將獲取到該方法的資訊例項

        2. 可以通過BindingFlags來設定過濾條件,可以獲取到公共的、私有的、受保護的、靜態的等屬性、方法或事件等資訊,比如:

  • MethodInfo[] mis =typeof(SomeClass).GetMethods(BindingFlags.Public | BindingFlags.Static);該方法可以獲取到SomeClass中公共的靜態方法。

       3. 可以通過System.Type的一個抽象的方法FindMembers設定引數MemberType、BindingFlags等查詢到程式集中想要的資訊,比如:

  • MemberInfo[] memInfo = typeof(SomeClass).FindMembers(MemberTypes.Field,BindingFlags.NonPublic | BindingFlags.Instance,null,null);

foreach(MemberInfo m in memInfo)
{
fi 
= m as FieldInfo;
if(fi !=null)
{
Console.WriteLine(
"{0} of value:{1}",fi.Name,fi.GetValue(ac));
}

}

該方法獲取到了類例項中的私有欄位並顯示出它們的值。

另外,一旦你發現目標成員變數,就可以把它們轉化為真正的FieldInfo物件,這樣你就可以查詢它們的值了。

最後,我們可以通過查詢到的程式集中資訊執行其中的一些操作(比如執行其中的一些方法)。這就是後期繫結的結果:在執行的時候定位並且執行一個型別(建立該型別的例項或者執行其方法,而此型別在你設計之初並不知曉)。

執行發現的程式碼的過程基本上要遵循以下幾個基本的步驟:

  • 載入程式集
  • 找到你希望使用的型別或者類
  • 建立該型別(或者類)的一個例項
  •  找到你希望執行的該型別的某個方法
  • 得到該方法的引數
  • 呼叫該物件上的方法並且傳遞給它恰當的引數

一旦找到要找的型別,就可以使用System.Activator建立此型別的一個例項。使用Activator類的方法CreateInstance(允許指定你想要建立的物件,並且可選擇的引數會應用到該物件的構造器上)建立一個類的例項,比如:object obj = System.Activator.CreateInstance(typeof(SomeClass));。

在使用CreateInstance建立類的例項時,可以通過帶引數的構造方法來獲取類的例項。首先是查詢帶引數的構造器,只要確定了構造器,就可以使用類ConstructorInfo的方法GetParameters得到其引數。GetParameters會返回一個ParameterInfo物件陣列,它將會幫助你確定引數的順序,引數的名稱以及引數的型別。隨後就可以建立引數值的陣列,然後把它傳遞給CreateInstance方法。比如:

假定你擁有SomeClass的構造器的一個引用(名稱為ci):

ParameterInfo[] pi = ci.GetParameters();
object[] param = new object[pi.Length];
foreach(ParameterInfo p in pi)
{
    if(p.ParameterType == typeof(string))
        param[p.Position] = "test";
}
object o = System.Activator.CreateInstance(typeof(SomeClass),param);
獲取到了類的例項之後,然後獲取類中的方法,具體操作已經在上面講過,例子如下:

MethodInfo mi = typeof(SomeClass).GetMethod("SomeMethod");

現在,擁有了類的例項,也找到了類中的方法,接下來就可以使用MethodInfo.Invoke呼叫你的目標方法了,需要傳遞包含該方法的物件的例項和該方法需要的一組引數的值,比如:mi.Invoke(o,null);


第二部分:

這一部分的內容是反射的高階功能:Emit,即反射發出,它具有在執行時動態的產生程式碼的功效。它允許你從零開始,動態的構建程式集和型別的所有框架類的根。在需要時動態的產生程式碼。

注意:反射發出(reflection emit)並不能產生原始碼。換句話說,你在這裡的努力並不能建立C#程式碼。相反,反射發出(reflection emit)類會建立MSIL op程式碼。
使用反射發出(reflection emit)要遵循的過程

1. 建立一個新的程式集(程式集是動態的存在於記憶體中或把它們持久化到磁碟上)。

2. 在程式集內部,建立一個模組(module)。

3. 在模組內部,建立一個型別。

4.  給型別新增屬性和方法。

5.  產生屬性和方法內部的程式碼

第一步:構建程式集

在實際的操作中,第一步要遵循如下幾個過程:

a)   建立一個AssemblyName(用於唯一標識和命名程式集),比如:AssemblyName name =new AssemblyName();name.Name ="MyAssembly";

b)   獲取當前應用程式域的一個引用(使用應用程式域提供的方法,返回AssemblyBuilder物件),比如:AppDomain ad = System.Threading.Thread.GetDomain();

c)    通過呼叫AppDomain.DefineDynamicAssembly產生一個AssemblyBuilder物件例項,比如:AssemblyBuilder builder= ad.DefineDynamicAssembly(name,AssemblyBuilderAccess.Run);。其中AssemblyBuilderAccess列舉值表明,是想把程式集寫入磁碟,儲存到記憶體,還是兩者都有。該方法中似要把程式集儲存在記憶體裡。

在上述的幾個步驟中,AssemblyBuilder類是整個反射發出的工作支架,它提供了從零開始構造一個新的程式集的主要機制。
第二步:定義一個模組(module)

在這個步驟中,要在上個步驟中建立的AssemblyBuilder類例項中建立ModuleBuilder類例項。ModuleBuilder用於在一個程式集中建立一個模組。呼叫AssemblyBuilder物件上的DefineDynamicModule方法將會返回一個ModuleBuilder物件例項。在該方法中必須給這個模組命名(在這裡,名字僅僅是個字串)。比如:

ModuleBuilder mb =  builder.DefineDynamicModule("MyModule");

第三步:建立一個型別

在這個步驟中,需要使用一個方法(DefineType)從ModuleBuilder類例項中得到一個TypeBuilder物件的例項。

TypeBuilder theClass = mb.DefineType("MathOps",TypeAttributes.Public & TypeAttributes.Class);
該方法中使用TypeAttributes列舉指定了該型別的可見度為公共的。

第四步:新增一個方法

在這個步驟中,可以給上一步驟創建出來的類的例項中建立方法。

使用MethodBuilder類可以為你指定的型別定義方法。你可以在之前建立的型別物件上(theClass)呼叫DefineMethod獲取一個MethodBuilder例項的引用。DefineMethod攜帶四個引數:方法的名稱,方法可能的屬性(如:public,private等等),方法的引數以及方法的返回值。如果沒有引數和返回值,可以設定引數和返回值為void。

為了定義返回值的型別,建立一個包含返回型別值的型別物件(一個System.Int32型別的值):Type ret =typeof(System.Int32);

使用型別值陣列定義方法的引數,這兩個引數也是Int32的型別值Type[] param =new Type[2]; param[0=typeof(System.Int32); param[1=typeof(System.Int32);

有了這些值,你現在就可以呼叫DefineMethod方法了:MethodBuilder methodBuilder = theClass.DefineMethod("ReturnSum",MethodAttributes.Public,ret,param);

第五步:產生程式碼

在這個步驟中,開始新增方法的內部程式碼了。這是使用反射發出(reflection emit)產生程式碼的過程中真正核心的部分。

有一點是需要注意的,反射發出(reflection emit)的類不能產生原始碼。換句話說,這裡的結果並不會產生C#程式碼,而是產生MSIL op 程式碼。MSIL(微軟中間語言)是一種接近於彙編程式的中間程式碼語言。當.NET JIT 編譯器產生本地二進位制程式碼的時候,就需要編譯MSIL。Op程式碼是低階的,類似於彙編程式的操作指令。

考慮方法ReturnSum的如下實現:
publicint ReturnSum(int val1,int val2)
{
    return val1 + val2;
}

如果你想“發出”這一段程式碼,你首先需要知道如何僅使用MSIL op程式碼編寫這個方法。值得高興的是,這裡有一個快速,簡單的辦法可以做到。你可以簡單的編譯一下這段程式碼,然後使用.NET框架裡的實用工具ildasm.exe檢視程式集的結果。以下MSIL版本的程式碼是編譯上面的方法產生的:
.method public hidebysig instance int32  ReturnSum(int32 val1, int32 val2) cil managed
{.maxstack  2
.locals init ([
0] int32 CS$00000003$00000000)
IL_0000:  ldarg.
1
IL_0001:  ldarg.
2
IL_0002:  add
IL_0003:  stloc.
0
IL_0004:  br.s       IL_0006
IL_0006:  ldloc.
0
IL_0007:  ret
}

為了產生這段程式碼,你需要使用ILGenerator類。你可以呼叫MethodBuilder.GetILGenerator()方法獲取對應方法上的ILGenerator類的一個例項:ILGenerator gen = methodBuilder.GetILGenerator();

使用gen物件,你可以把op指令注入到你的方法裡:

gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Ldarg_2);
gen.Emit(OpCodes.Add);
gen.Emit(OpCodes.Stloc_0);
gen.Emit(OpCodes.Br_S);
gen.Emit(OpCodes.Ldloc_0);
gen.Emit(OpCodes.Ret);

到此,你已經建立了方法,類,模組和程式集。為了得到這個類的一個引用,你可以呼叫CreateType,類似於下面的程式碼:theClass.CreateType();

Reflection.Emit的名稱空間和類:

Namespace.Class

System.Reflection.Emit.AssemblyBuilder

主要用途

定義動態的.NET程式集:一種自我描述的 .NET內建塊.動態程式集是通過反射發出特意產生的. 該類繼承於System.Reflection.Assembly.

範例

Dim ab As AssemblyBuilderDim ad As AppDomainad = Thread.GetDomain()ab = ad.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run)

Namespace.Class

System.Reflection.Emit.ConstructorBuilder

主要用途

用於建立和宣告一個動態類的構造器.它囊括了有關構造器的所有資訊,包括:名稱,方法簽名和主體程式碼.僅僅在你需要建立一個帶引數的構造器或者需要覆蓋父類構造器的預設行為的時候.

範例

Dim ourClass As TypeBuilder = [module].DefineType("ourClass", _TypeAttributes.Public)Dim ctorArgs As Type() = {GetType(String)}Dim ctor As ConstructorBuilder = _ourClass.DefineConstructor(MethodAttributes.Public, _CallingConventions.Standard, constructorArgs)

Namespace.Class

System.Reflection.Emit.CustomAttributeBuilder

主要用途

用於建立動態類的自定義特性.

Namespace.Class

System.Reflection.Emit.EnumBuilder

主要用途

定義和宣告列舉.

Namespace.Class

System.Reflection.Emit.EventBuilder

主要用途

為動態類建立事件.

Namespace.Class

System.Reflection.Emit.FieldBuilder

主要用途

為動態類建立欄位.

Namespace.Class

System.Reflection.Emit.ILGenerator

主要用途

用於產生MSIL程式碼.

範例

Dim gen As ILGenerator = someMethod.GetILGenerator()gen.Emit(OpCodes.Ldarg_0)gen.Emit(OpCodes.Ret)

Namespace.Class

System.Reflection.Emit.LocalBuilder

主要用途

建立方法或構造器的區域性變數.

Namespace.Class

System.Reflection.Emit.MethodBuilder

主要用途

用於建立和宣告動態類的方法.

Namespace.Class

System.Reflection.Emit.MethodRental

主要用途

一個很實用的類,用於從別的類中交換一個方法到動態建立的類中。當你需要快速重建一個已經在其它地方存在的方法時,就顯得非常有用。

Namespace.Class

System.Reflection.Emit.ParameterBuilder

主要用途

為方法的簽名建立引數.

Namespace.Class

System.Reflection.Emit.PropertyBuilder

主要用途

為動態的型別建立屬性.

把反射發出和動態呼叫結合起來

現在你已經知道,如何使用反射類“發出”一個動態的程式集,那麼讓我們把反射發出和動態呼叫的內容(在第一部分講到的)結合起來。

舉個例子,在執行時何時使用Reflection和Reflection.Emit勝過程式碼或指令碼賦值呢?這是有可能的,例如,顯示一個帶有輸入框的窗體,要求使用者輸入一個公式,然後在執行時通過編譯後的程式碼,求這個公式的值。


另外一種使用Reflection.Emit的時候,是為了使效能達到最優化。針對某一個問題,編碼的解決方案,有時候故意的趨向於通用的解決方案。從設計的角度出發,這通常都是一件好事情,因為這會使你的系統更具有靈活性。例如,如果你想計算一些數字的和,在你設計的時候不必關心有多少個數字需要求和,因此你需要呼叫一個迴圈來解決這樣的問題。如果你重寫ReturnSum方法,讓它接收一個Integer型的陣列,你就需要在這個陣列的成員之間迴圈,把每一個加到計數器上,然後返回所有數字的求和值。這是一個非常好的,通用的解決方法,因為它不必關心包含在陣列中的值。

publicint ReturnSum(int[] values)
{
        
int retVal;
        
for(int i=0;i<values.Length;i++)
        
{
            retVal 
+= values[i];
        }
return retVal;
}

另一方面,如果你硬編碼陣列的界限,那麼你就可以通過編寫一個長的數字操作語句來求和,這樣的方式將會使程式碼達到更優化的狀態。對於少量的值甚至幾百個值而言,這兩種編寫方式帶來的效能上的差距是可以忽略的。但是,如果你正在處理數千或者數百萬的值,硬編碼的方式將會非常非常的快。事實上,你可以把這個方法編寫得更快,直接把陣列中的值取出來相加,同時,把不影響結果的零值去掉。

publicint ReturnSum()
{
    
return9+32+8+1+2+2+90;}

當然,這裡的問題是,你編寫的程式碼是不通用的和沒有靈活性的。


因此,如何能夠同時得到這兩者的優點呢?答案是:使用Reflection.Emit。

通過把Reflection.Emit的功能(接收陣列的上限和陣列的值,然後產生數字直接相加的程式碼)和Reflection的功能(定位,載入並執行發出的程式集)融合在一起,你將能夠打造出優雅的,具有獨創性的效能解決方案,從而很好的避免了脆弱的程式碼。在這個簡單的例子裡,你可以寫一個迴圈語句,產生你需要的MSIL op程式碼。


考慮下面的控制檯程式,它接收一個數組,並建立一個新的程式集,模組,類和ReturnSum方法,它(ReturnSum)將直接求和陣列中的值,而不是使用迴圈。程式碼如下:

using System;
using System.Data;
using System.Reflection;
using System.Reflection.Emit;
namespace ConsoleApplicationReflection
{
class MathClassBuilder
{
///<summary>/// 應用程式的主入口點。
///</summary>

        [STAThread]
staticvoid Main(string[] args)
{
try{
                Console.WriteLine("Enter values:");
string numbers = Console.ReadLine();
string[] values = numbers.Split(',');
                Type MathOpsClass = CreateType("OurAssembly","OurModule""MathOps""ReturnSum", values);
object  MathOpsInst = Activator.CreateInstance(MathOpsClass);
object obj = MathOpsClass.InvokeMember("ReturnSum",BindingFlags.InvokeMethod,null,MathOpsInst,null);
                Console.WriteLine("Sum: {0}",obj.ToString());
            }
catch(Exception ex)
{
                Console.WriteLine("An error occured: {0}",ex.Message);
            }
            Console.ReadLine();
        }publicstatic Type CreateType(string assemblyName,string moduleName,string className,string methodName,string[] values)
{
try{
                AssemblyName name =new AssemblyName();
                name.Name = assemblyName;
                AppDomain domain = System.Threading.Thread.GetDomain();
                AssemblyBuilder assBuilder = domain.DefineDynamicAssembly(name,AssemblyBuilderAccess.Run);
                ModuleBuilder mb = assBuilder.DefineDynamicModule(moduleName);
                TypeBuilder theClass = mb.DefineType(className,TypeAttributes.Public | TypeAttributes.Class);
                Type rtnType =typeof(int);
                MethodBuilder method = theClass.DefineMethod(methodName,MethodAttributes.Public,rtnType,null);
                ILGenerator gen = method.GetILGenerator();
                gen.Emit(OpCodes.Ldc_I4,0);
for(int i=0;i<values.Length;i++)
{
                    gen.Emit(OpCodes.Ldc_I4,int.Parse(values[i]));
                    gen.Emit(OpCodes.Add);
                }
                gen.Emit(OpCodes.Ret);return theClass.CreateType();
            }
catch(Exception ex)
{
                Console.WriteLine("An error occured: {0}",ex.Message);
returnnull;
            }
      }
    }
}