C#反射
反射初步認識
作為一名C#小白,在學習C#的過程中,遇到了不太懂的内容,專門整理了一下在網上搜集到的資料。
反射的定義
反射提供了封裝程式集、子產品和類型的對象(Type類型)。可以使用反射動态建立類型的執行個體,将類型綁定到現有對象,或從現有對象擷取類型并調用其方法或通路其字段和屬性。如果代碼中使用了特性,可以使用反射對他們進行通路。
- 中繼資料的動态查詢;
- 綁定與執行;
- 動态代碼生成;
使用場景
- 需要通路程式中繼資料的特性
- 檢查和執行個體化程式集中的類型
- 在運作時建構新類型。使用System.Reflection.Emit中的類
- 執行後期綁定,通路在運作時建立的類型的方法
反射用到的命名空間
- System.Reflection
- System.Type
- System.Reflection.Assembly
反射包括
- 程式集反射
- 類型反射
- 接口反射
利用反射實作延遲加載程式集
所有對程式集的引用都是在編譯時進行的,是以效率會比較高。但是在某些特定的情況下,我們需要對程式集進行延遲加載,即對程式集的引用由編譯時推移到運作時,反射是一個很好的選擇。
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類型的觀察,我們可以得到這樣一個接口,我們先看接口的實作,并将其編譯為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接口的就可以了,這樣在運作時就不需要進行大量的校驗工作了,這些工作都還原到了編譯時,是以使用接口來實作反射也大大提高了反射的性能。
System.Type.GetType()和Object.GetType()與typeof比較
//運算符,獲得某一類型的 System.Type 對象。
Type t = typeof(int);
//方法,擷取目前執行個體的類型。
int i = 10;
Console.WriteLine(i.GetType());
//差別
Typeof()是運算符而GetType是方法
GetType()是基類System.Object的方法,是以隻有建立一個執行個體之後才能被調用(也就是建立執行個體)
Typeof()的參數隻能是lint,string,類,且不能是執行個體
得到結果的差別
(1)Typeof():得到一個class的Type
(2)GetType():得到一個class執行個體的Type
System.Type.GetType()的使用
Type type = System.Type.GetType(“ConsoleApplication1.child”);
Type type1 = System.Type.GetType(“System.Int32”);
Object.GetType()的小案例
public class Student
{
public Student()
{
}
public virtual string Id { get; set; }
public virtual string StudentNo { get; set; }
public virtual string Address { get; set; }
}
public class StudentDTO
{
public StudentDTO()
{
}
public virtual string Id { get; set; }
public virtual string StudentNo { get; set; }
public virtual int TeacherId { get; set; }
}
//對student對象指派
Student student = new Student();
student.Id = Guid.NewGuid().ToString();
student.Name = "張三";
student.Address = "福建";
//将student的值賦予studentdto
StudentDTO studentDTO = new StudentDTO();
studentDTO.Id = student.Id;
studentDTO.Name = student.Name;
改進:若是student的屬性過多,那麼可以通過此方法減少許多代碼
foreach (var item in student.GetType().GetProperties()) //傳回Student的所有公共屬性
{
var value = item.GetValue(student, null); //傳回屬性值
var setobj = studentDTO.GetType().GetProperty(item.Name); //搜尋具有指定屬性名稱的公共屬性
if (value != null && setobj != null)
{
setobj.SetValue(studentDTO, value, null);
}
}
Assembly.Load、LoadFrom與LoadFile
- Assembly.Load()
簡介
Load()方法接收一個String或AssemblyName類型作為參數,這個參數實際上是需要加載的程式集的強名稱(名稱,版本,語言,公鑰标記)。例如.NET 2.0中的FileIOPermission類,它的強名稱是:
System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
對于弱命名的程式集,則隻會有程式集名稱,而不會有版本,語言和公鑰标記。如 TestClassLibrary
細節
- CLR内部普遍使用了Load()方法來加載程式集,在Load()方法的内部,CLR首先會應用這個程式集的版本綁定重定向政策,接着在GAC中查找目标程式集。如果GAC中沒有找到,則會在應用程式目錄和子目錄中尋找(應用配置檔案的codebase所指定的位置)。
- 如果希望加載弱命名程式集,Load()方法就不會去GAC中查找。
- 當Load()找到目标程式集時,就會加載它,并傳回一個相應Assembly對象的引用。
- 當沒有找到程式集時,會抛出System.IO.FileNotFoundException異常。
- 當存在特定CPU架構的程式集時,CLR會優先加載目前架構的程式集(例如x86版本優先于IL中立版本)
- 如果希望強迫加載某個架構版本的程式集,需要在強名稱中加以指定。ProcessorArchitecture可以為x86 IA64 AMD64或MSIL,當然還有None
- Load方法與Win32函數中的LoadLibrary方法等價
- Assembly.LoadFrom()
簡介
LoadFrom()方法可以從指定檔案中加載程式集,通過查找程式集的AssemblyRef中繼資料表,得知所有引用和需要的程式集,然後在内部調用Load()方法進行加載。
Assembly.LoadFrom(@“C:\ABC\Test.dll”);
細節
- LoadFrom()首先會打開程式集檔案,通過GetAssemblyName方法得到程式集名稱,然後關閉檔案,最後将得到的AssemblyName對象傳入Load()方法中
- 随後,Load()方法會再次打開這個檔案進行加載。是以,LoadFrom()加載一個程式集時,會多次打開檔案,造成了效率低下的現象(與Load相比)。
- 由于内部調用了Load(),是以LoadFrom()方法還是會應用版本綁定重定向政策,也會在GAC和各個指定位置中進行查找。
- LoadFrom()會直接傳回Load()的結果——一個Assembly對象的引用。
- 如果目标程式集已經加載過,LoadFrom()不會重新進行加載。
- LoadFrom支援從一個URL加載程式集(如"http://www.abc.com/test.dll"),這個程式集會被下載下傳到使用者緩存檔案夾中。
- 從URL加載程式集時,如果計算機未聯網,LoadFrom會抛出一個異常。如果IE被設定為“脫機工作”,則不會抛出異常,轉而從緩存中尋找已下載下傳的檔案。
- Assembly.LoadFile()
簡介
LoadFile()從一個指定檔案中加載程式集,它和LoadFrom()的不同之處在于LoadFile()不會加載目标程式集所引用和依賴的其他程式集。您需要自己控制并顯示加載所有依賴的程式集
細節
- LoadFile()不會解析任何依賴
- LoadFile()可以多次加載同一程式集
- 顯式加載依賴程式集的方法是,注冊AppDomain的AssemblyResolve事件
對于反射的總結,我想從以下幾個方面展開,首先是反射程式集,子產品,類的成員以及成員的一些資訊;接下來就是動态調用類的成員方法;第三個方面就動态産生程式集,子產品和類以及類的成員。好了,現在就讓我們從反射各種資訊開始吧
在C#中,我們要使用反射,首先要搞清楚以下命名空間中幾個類的關系:
System.Reflection命名空間
(1) AppDomain:應用程式域,可以将其了解為一組程式集的邏輯容器
(2) Assembly:程式集類
(3) Module:子產品類
(4) Type:使用反射得到類型資訊的最核心的類
他們之間是一種從屬關系,也就是說,一個AppDomain可以包含N個Assembly,一個Assembly可以包含N個Module,而一個Module可以包含N個Type.
AppDomain這個類我們等下再來講解。我們先關注Assembly個類
在程式中,如果我們要動态加載一個程式集怎麼辦呢?有幾種方式可以使用,分别是Load、LoadFrom和LoadWithPartialName三個Assembly的靜态方法.
先來講解Assembly.Load方法,該方法會有多個重載版本,其中一個就是提供程式集的詳細資訊,即程式集的辨別,包括程式集的名稱,版本,區域資訊,公有密鑰标記,全部都是以一個字元串的形式提供,例如:"MyAssembly,Version=1.0.0.0,culture=zh-CN,PublicKeyToken=47887f89771bc57f”.
那麼,使用Assembly.Load加載程式集的順序是怎樣的呢?首先它會去全局程式集緩存查找,然後到應用程式的根目錄查找,最後會到應用程式的私有路徑查找。
當然,如果你使用的是弱命名程式集,也即隻給出程式集的名稱,那麼這個時候,CLR将不會在程式集上應用任何安全或者部署政策,而且Load也不會到全局緩存程式集中查找程式集。
測試加載弱命名程式集的例子如下:
(1) 建立一個控制台應用程式的工程,同時勾選建立解決方案
(2) 在解決方案中建立一個類庫的項目,随便寫一個類和一個方法
(3) 在控制台項目中,首先不添加引用,直接在Main方法中添加如下代碼:
Assembly assembly = Assembly.Load(“MyAssembly”);
if (assembly != null)
{ Console.WriteLine(“加載成功”); }
執行程式,會抛出異常,說找不到該程式集。什麼原因呢?因為我們使用的是弱命名程式集,Load方法不會去全局程式集緩存中查找,而該應用程式目錄下又沒有該程式集,是以程式找不到。這個時候,我們把程式稍微改一下,不用添加代碼,隻需添加對MyAssembly的引用,重新運作程式,加載成功了。
接下來,我們就要看看Load怎麼加載強命名程式集了,這個步驟稍微有些複雜。還是剛才的項目,找到MyAssembly.dll程式集所在的目錄,一般在bin"Debug目錄下
(1)生成密鑰對檔案 sn –k MyAssemblyKey.keys
你也可以自己随便起一個密鑰對檔案名
(2)生成公鑰檔案
sn –p MyAssemblyKey.keys MyAssemblyPublicKey.PublicKey
注:檢視公鑰指令:sn –tp MyAssemblyPublicKey.PublicKey
(3)建立強命名程式集。
很簡單,隻需要在聲明命名空間的那句代碼上加上如下特性:
[assembly:AssemblyKeyFileAttribute(@”D:"Test"MyAssemblyKey.keys”)]
(4) 編譯項目
(5) 将程式集添加到程式集全局緩存
gacutil –i MyAssembly.dll
這個時候,轉到加載程式集的項目中,将Load方法中的參數改為”程式集名,Version=版本,culture=區域資訊,PublicKeyToken=公鑰“,然後再去掉對程式集的引用,我們會發現,程式運作成功。表明Load到全局緩存區查找到了該程式集。
使用Load方法加載程式集,特别是強命名程式集,能在程式集上應用安全和部署政策,推薦使用該方法動态加載程式集,至于LoadFrom和LoadWithPartialName。
首先我們還是來看看LoadFrom方法,這個方法的原理是這樣的:我們如果要使用它來動态加載程式集,必須告訴它程式集的路徑,也即在哪個目錄下面,CLR會去加載與你指定的路徑完全比對的程式集。記住,當我們指定程式集路徑時,不能包括任何關于程式集強命名的資訊,是以,CLR不會在我們指定的程式集檔案上應用任何政策,而且也不會去任何其他的地方搜尋程式集,簡言之,它就是指哪打哪,呵呵。
例如:你有個程式集在D:/Test/MyAssembly.dll,你要用Assembly.LoadFrom加載該程式集,代碼就如下:
Assembly assembly = Assembly.LoadFrom(@”D:/Test/MyAssembly.dll”);
對于,LoadWithParitalName方法,推薦大家最好不要使用它,因為程式無法确定最終要去加載哪個程式集的版本,是以我們這裡隻是簡單的介紹一下它的工作原理:你可以傳遞一個程式集辨別給它,包括程式集名稱,至于其他資訊是可選的(區域資訊,公有密鑰等),該方法執行時,會首先檢查應用程式中配置檔案的qualifyAssembly節點,如果存在,則把該部分名稱的程式集替換成完全的程式集辨別,如果不存在,則使用程式集名稱先到應用程式根目錄下查找,然後是私有目錄,沒有找到的話,就到程式集全局緩存中查找。簡單過程如下:
應用程式根目錄 -> 應用程式私有目錄 -> 程式集全局緩存.
Assembly.Load()方法,Assembly.LoadFrom()方法,Assembly.LoadFile()方法的差別!
1,Assembly.Load()
這個方法通過程式集的長名稱(包括程式集名,版本資訊,語言文化,公鑰标記)來加載程式集的,會加載此程式集引用的其他程式集,一般情況下都應該優先使用 這個方法,他的執行效率比LoadFrom要高很多,而且不會造成重複加載的問題(原因在第2點上說明)
使用這個方法的時候, CLR會應用一定的政策來查找程式集,實際上CLR按如下的順序來定位程式集:
⑴如果程式集有強名稱,在首先在全局程式集緩(GAC)中查找程式集。
⑵如果程式集的強名稱沒有正确指定或GAC中找不到,那麼通過配置檔案中的元素指定的URL來查找
⑶如果沒有指定強名稱或是在GAC中找不到,CLR會探測特定的檔案夾:
假設你的應用程式目錄是C:\AppDir,元素中的privatePath指定了一個路徑Path1,你要定位的程式集是AssemblyName.dll則CLR将按照如下順序定位程式集
C:\AppDir\AssemblyName.dll
C:\AppDir\AssemblyName\AssemblyName.dll
C:\AppDir\Path1\AssemblyName.dll
C:\AppDir\Path1\AssemblyName\AssemblyName.dll
如果以上方法不能找到程式集,會發生編譯錯誤,如果是動态加載程式集,會在運作時抛出異常!
2,Assembly.LoadFrom()
這個方法從指定的路徑來加載程式集,實際上這個方法被調用的時候,CLR會打開這個檔案,擷取其中的程式集版本,語言文化,公鑰标記等資訊,把他們傳遞給 Load方法,接着,Load方法采用上面的政策來查找程式集。如果找到了程式集,會和LoadFrom方法中指定的路徑做比較,如果路徑相同,該程式集會被認為是應用程式的一部分,如果路徑不同或Load方法沒有找到程式集,那該程式集隻是被作為一個“資料檔案”來加載,不會被認為是應用程式的一部分。 這就是在第1點中提到的Load方法比LoadFrom方法的執行效率高的原因。另外,由于可能把程式集作為“資料檔案”來加載,是以使用 LoadFrom從不同路徑加載相同程式集的時候會導緻重複加載。當然這個方法會加載此程式集引用的其他程式集。
3,Assembly.LoadFile()
這個方法是從指定的檔案來加載程式集,和上面方法的不同之處是這個方法不會加載此程式集引用的其他程式集!
結論:一般大家應該優先選擇Load方法來加載程式集,如果遇到需要使用LoadFrom方法的時候,最好改變設計而用Load方法來代替!
另:Assembly.LoadFile 與 Assembly.LoadFrom的差別
1、Assembly.LoadFile隻載入相應的dll檔案,比如Assembly.LoadFile(“abc.dll”),則載入abc.dll,假如abc.dll中引用了def.dll的話,def.dll并不會被載入。
Assembly.LoadFrom則不一樣,它會載入dll檔案及其引用的其他dll,比如上面的例子,def.dll也會被載入。
2、用Assembly.LoadFrom載入一個Assembly時,會先檢查前面是否已經載入過相同名字的Assembly,比如abc.dll有兩個版本(版本1在目錄1下,版本2放在目錄2下),程式一開始時載入了版本1,當使用Assembly.LoadFrom(“2\abc.dll”)載入版本2時,不能載入,而是傳回版本1。Assembly.LoadFile的話則不會做這樣的檢查,比如上面的例子換成Assembly.LoadFile的話,則能正确載入版本2。
LoadFile:加載指定路徑上的程式集檔案的内容。LoadFrom: 根據程式集的檔案名加載程式集檔案的内容。
差別:
LoadFile 方法用來來加載和檢查具有相同辨別但位于不同路徑中的程式集.但不會加載程式的依賴項。
LoadFrom 不能用于加載辨別相同但路徑不同的程式集。
GetMembers、GetField和GetMethod
public class RefClass
{
private int _test3;
private int _test1 { get; set; }
protected int Test2 { get; set; }
public int Test3 { get; set; }
private static void Show2()
{
}
public static void Show3()
{
}
public void Show()
{
}
}
static void Main(string[] args)
{
Type t = typeof(RefClass);
Func<MemberTypes, String> getType = (x) =>
{
switch (x)
{
case MemberTypes.Field:
{
return "字段";
}
case MemberTypes.Method:
{
return "方法";
}
case MemberTypes.Property:
{
return "屬性";
}
default:
{
return "未知";
}
}
};
MemberInfo[] minfos = t.GetMembers(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static );
foreach (MemberInfo minfo in minfos)
{
Console.WriteLine(minfo.Name + ";類型:" + getType(minfo.MemberType));
}
Console.ReadKey();
}
1 static void Main(string[] args)
2 {
3 Type t = typeof(RefClass);
4 FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
5 foreach (FieldInfo finfo in finfos)
6 {
7 Console.WriteLine("字段名稱:{0} 字段類型:{1} ", finfo.Name, finfo.FieldType.ToString());
8 }
9 Console.ReadKey();
10 }
1 static void Main(string[] args)
2 {
3 Type t = typeof(RefClass);
4 RefClass rc = new RefClass();
5 rc.Test3 = 3;
6 FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
7 foreach (FieldInfo finfo in finfos)
8 {
9 Console.WriteLine("字段名稱:{0} 字段類型:{1} rc中的值為:{2}", finfo.Name, finfo.FieldType.ToString(), finfo.GetValue(rc));
10 }
11 Console.ReadKey();
12 }
1 static void Main(string[] args)
2 {
3 Type t = typeof(RefClass);
4 RefClass rc = new RefClass();
5 rc.Test3 = 3;
6 FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
7 foreach (FieldInfo finfo in finfos)
8 {
9 finfo.SetValue(rc, 100);
10 Console.WriteLine("字段名稱:{0} 字段類型:{1} rc中的值為:{2}", finfo.Name, finfo.FieldType.ToString(), finfo.GetValue(rc));
11 }
12 Console.ReadKey();
13 }
1 static void Main(string[] args)
2 {
3 Type t = typeof(RefClass);
4 RefClass rc = new RefClass();
5 rc.Test3 = 3;
6 PropertyInfo[] finfos = t.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
7 foreach (PropertyInfo finfo in finfos)
8 {
9 MethodInfo getinfo = finfo.GetGetMethod(true);
10 Console.WriteLine("get方法的名稱{0} 傳回值類型:{1} 參數數量:{2} MSIL代碼長度:{3} 局部變量數量:{4}", getinfo.Name, getinfo.ReturnType.ToString(),
11 getinfo.GetParameters().Count(),
12 getinfo.GetMethodBody().GetILAsByteArray().Length,
13 getinfo.GetMethodBody().LocalVariables.Count);
14
15 MethodInfo setinfo = finfo.GetSetMethod(true);
16 Console.WriteLine("get方法的名稱{0} 傳回值類型:{1} 參數數量:{2} MSIL代碼長度:{3} 局部變量數量:{4}", setinfo.Name, setinfo.ReturnType.ToString(),
17 setinfo.GetParameters().Count(),
18 setinfo.GetMethodBody().GetILAsByteArray().Length,
19 setinfo.GetMethodBody().LocalVariables.Count);
20
21 setinfo.Invoke(rc, new object[] { 123 });
22 object obj = getinfo.Invoke(rc, null);
23 Console.WriteLine("方法名:{0} 内部值:{1}", finfo.Name, obj);
24 }
25 Console.ReadKey();
26 }
1 public class RefClass
2 {
3 private int _test3;
4 private int _test1 { get; set; }
5 protected int Test2 { get; set; }
6 public int Test3 { get; set; }
7
8 private static void Show2()
9 {
10
11 }
12
13 public static string Show3(string s)
14 {
15 int b;
16 int c;
17 return s;
18 }
19
20 public string Show(string s)
21 {
22 string a;
23 return s;
24 }
25 }
1 static void Main(string[] args)
2 {
3 Type t = typeof(RefClass);
4 RefClass rc = new RefClass();
5 rc.Test3 = 3;
6 MethodInfo[] finfos = t.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Static );
7 foreach (MethodInfo finfo in finfos)
8 {
9 if (finfo.GetParameters().Count() > 0 && finfo.GetParameters()[0].ParameterType == typeof(string) )
10 {
11 object obj = finfo.Invoke(rc, new[] { "123" });
12 MethodBody mbody = finfo.GetMethodBody();
13 Console.WriteLine("擁有參數的方法名:{0} 傳回值類型:{1} 參數1類型:{2} 參數1名稱:{3} 方法調用後傳回的值:{4}",
14 finfo.Name,
15 finfo.ReturnType.ToString(),
16 finfo.GetParameters()[0].ParameterType.ToString(),
17 finfo.GetParameters()[0].Name,
18 obj.ToString());
19 }
20 else
21 {
22 MethodBody mbody = finfo.GetMethodBody();
23 Console.WriteLine("沒有參數的方法名:{0} 傳回值類型:{1}",
24 finfo.Name,
25 finfo.ReturnType.ToString());
26 }
27 }
28 Console.ReadKey();
29 }