反射(原創推薦)
在程序中,當我們需要動態的去加載程序集的時候(將對程序集的引用由編譯時推移到運行時),反射是一種很好的選擇。反射為.NET類型提供了高度的動態能力,包括:元數據的動態查詢、綁定與執行、動態代碼生成。常用的反射類型包含在System.Reflection和System.Reflection.Emit,反射包括程序集反射、類型反射、接口反射、類型成員反射。
編譯時加載程序集
下面先從一個簡單的例子說起,假如我們有一個Point類如下所示:
using System;
public class Point
{
public int x;
public int y;
public void Print()
{
Console.WriteLine("[{0},{1}]",x,y);
}
}
先將其編譯成Point.dll文件,然后我們需要在Reflect.cs文件中用到這個類型:
代碼如下:
using System;
public class Reflect
{
public static void Main()
{
Point p=new Point();
p.x=100;
p.y=200;
p.Print();
}
}
然后我們編譯這個文件,會發現拋出異常,告訴我們不存在Point類型,也就是說Point是一個未知類型,編譯器根本不知道它,那怎么辦了,這里我們需要在編譯時對Point程序集進行引用,這樣才能讓編譯器知道它,在很多應用程序開發框架中在我們編譯項目的時候框架會自動進行程序集的引用,但是在這里我們的編譯工作是手動進行的,因此我們也需要手動的引用程序集。下面是編譯命令:
csc /r:Point.dll Reflect.cs
這樣我們在編譯Reflect.cs文件的過程中就實現了對Point程序集的引用,這樣編譯器就識別了程序集中Point類型信息。其實任何類型信息的使用都需要有程序集的引用,不然的話編譯器會不認識這個類型,編譯就無法通過。我們可能會想,我們在很多時候進行編譯的時候并沒有對哪個程序集進行引用么?不是沒有,而是很多預定義的類型信息編譯器會默認幫我們進行引用,不需要我們顯示的進行引用,比如mscorlib程序集,它是.NET一個核心程序集,里面包含了很多常用的預定義的類型信息,因此C#編譯器在編譯的時候會默認對它進行引用,可能我們會想,我們怎么知道一段代碼在編譯的過程中它到底都引用了哪些程序集了,這里就需要借助于元數據,下面我們通過ildasm工具查看了編譯好的Reflect.exe程序集中的元數據,我們只截取了其中一段進行說明。
// Metadata version: v4.0.30319
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly extern Point
{
.ver 0:0:0:0
}
從這段元數據中我們可以看出,這個程序集中包含了對Point程序集和mscorlib程序集的引用。
利用反射實現延遲加載程序集
在上面所舉的例子中,所有對程序集的引用都是在編譯時進行的,因此效率會比較高。但是在某些特定的情況下,我們需要對程序集進行延遲加載,即將對程序集的引用由編譯時推移到運行時,反射是一種很好的選擇,在最開始我們講過,反射為.NET類型提供了高度的動態能力,包括:元數據的動態查詢、綁定與執行、動態代碼生成,這些功能的實現都離不開元數據。下面我們看看反射的具體實現。
using System;
using System.Reflection;
class Test
{
public static void Main(string[] args)
{
string assemblyName=args[0];
string typeName=args[1];
string fieldName1=args[2];
string fieldName2=args[3];
string methodName=args[4];
Assembly assembly=Assembly.Load(assemblyName); //手動加載程序集
Type type=assembly.GetType(typeName); //獲取程序集中的類型
//查詢
MemberInfo[] mis=type.GetMembers(); //獲取類型中的成員信息
for(int i=0;i<mis.Length;i++)
{
Console.WriteLine(mis[i]);
}
object obj=Activator.CreateInstance(type); //創建對象實例
//查詢字段
FieldInfo field1=type.GetField(fieldName1);
FieldInfo field2=type.GetField(fieldName2);
field1.SetValue(obj,100); //實例成員必須依附于對象實例才能賦值
field2.SetValue(obj,200);
//查詢方法
MethodInfo method=type.GetMethod(methodName);
//調用方法
method.Invoke(obj,null); //實例方法必須依附于對象實例才能執行
}
}
這里我們在編譯這段代碼的時候并沒有對Point程序集進行引用,而是將其推移到了運行時。在這段代碼中Point類型并不存在,因此我們并不能用Point類型來創建對象實例。但是下面的幾個操作是針對于對象的實例成員進行的,他們需要依附于對象的實例,因此我們在這里根據在運行時加載進來的類型信息創建了一個對象,但是編譯時類型信息是未知的。我們可以通過查看它的元數據得知它編譯后并沒有對Point程序集進行加載。
那么運行時怎么對程序集進行加載了,這里我們就需要將需要加載的程序集的信息在入口點函數中以參數的形式傳遞進來,下面是運行時的命令:

在這段命令中我們指出了運行時需要加載的程序集的名稱和其中的類型的名稱以及類型中的成員信息。由于類型信息在編譯時是未知的,因此我們并不清楚在運行時會加載進來什么樣的類型信息(需要通過傳入的參數確定)。也就是說我們現在所進行的一些操作是針對一個不確定的類型的(如果我們事先并不了解Point的類型信息)。而在上面的代碼中我們之所以能夠對加載進來類型信息進行這樣的處理,是因為我們事先已經對Point類型信息有所了解了,因此我們遵循了這樣一個隱式的約定來對類型進行操作。但是在很多情況下我們是并不了解未來加載進來的類型信息它所遵循的約定。因此在運行時,反射需要做很多的校驗工作,也就是說把本來應該在編譯時做的校驗工作都推移到了運行時,比如參數類型的兼容性以及所調用的方法是實例方法還是靜態方法,因此反射的效率比較低。
利用接口提高反射效率
那么怎么樣才能提高反射的效率了,很簡單,我們需要明確約定,也就是說只有滿足了約定信息的類型才能被加載進來,也因此我們對類型成員的處理必須是滿足了這一約定的,這樣雙方都有了一個共同的約定。那么我們用什么來實現這一約定了,當然需要用到接口了。
我們對上面的例子進行改進,通過對Point類型的觀察,我們可以得到這樣一個接口,我們先看接口的實現,并將其編譯為IPoint.dll文件。
using System;
public interface IPoint
{
public int X{set;get;}
public int Y{set;get;}
void Print();
}
下面我們來實現Point類,并對其進行編譯,注意:在編譯時確保對IPoint.dll的引用。
using System;
public class Point :IPoint
{
private int x;
private int y;
public int X
{
set{this.x=value;}
get{return x;}
}
public int Y
{
set{this.y=value;}
get{return y;}
}
public void Print()
{
Console.WriteLine("[{0},{1}]",this.X,this.Y);
}
}
下面我們再來看Reflect類的實現,在對其進行編譯時同樣需要對IPoint.dll進行引用。
using System;
using System.Reflection;
class Test
{
public static void Main(string[] args)
{
string assemblyName=args[0];
string typeName=args[1];
Assembly assembly=Assembly.Load(assemblyName); //手動加載程序集
Type type=assembly.GetType(typeName); //獲取程序集中的類型
IPoint obj=(IPoint)Activator.CreateInstance(type); //通過接口創建對象實例
obj.X=100;
obj.Y=200;
obj.Print();
}
}
最后運行編譯后的Reflect.exe文件,運行時同樣需要傳入程序集信息和類型信息。這里我們看到Point程序集和Reflect程序集在編譯時都對IPoint程序集進行了引用,因此在Reflect程序集中,雖然在編譯時并沒有Point類型信息,但是有IPoint接口信息,因此我們通過這個接口很方便的實現了我們需要的操作,只要未來加載進來了類型是實現了IPoint接口的就可以了,這樣在運行時就不需要進行大量的校驗工作了,這些工作都還原到了編譯時,因此使用接口來實現反射也大大提高了反射的性能。
聲明:本人接觸.NET時間不長,希望各位路過的高手要是覺得文中有錯誤的話能及時告訴我,本人力求最做到所有信息的正確性,謝謝。
浙公網安備 33010602011771號