反射總結目錄
什么是反射
程序運行時將exe、dll文件加載到內存並執行一些操作的過程,這個過程稱為
反射
。
通常所說的反射是實現這個過程所使用的技術手段,.Net中System.Reflection等命名空間提供了反射的實現。
反射的原理
通過對程序集元數據的搜索找到對應的成員類型並使用,以實現驗證或動態調用程序集的目的。
一個簡單的例子引入反射
下面這簡單例子引入反射的使用,這個例子中定義了一個Hello
類並添加一個Say
方法,我將使用反射調用Say
方法。
namespace ReflectionStudy
{
public class Hello
{
public void Say()
{
Console.WriteLine("Hello Reflection!");
}
}
}
//使用反射技術調用Say方法
//1.從當前程序集中查找Hello類
var helloType= Assembly.GetExecutingAssembly().GetType("ReflectionStudy.Hello");
//2.獲取Hello類的Say方法
var method = helloType.GetMethod("Say");
//3.創建Hello類的實例
var helloInstance=Activator.CreateInstance(helloType);
//4.執行Say方法
method.Invoke(helloInstance, null);
System.Reflection命名空間
上面的例子雖然簡單,但是已足夠說明反射的大致流程:
- 首先加載程序集
- 在程序集中查找我們需要的類(發現類型)
- 生成類實例
- 找到我們需要使用的
MethodInfo
,FiledInfo
,PropertyInfo
,EventInfo
等成員類型- 最后就是執行我們要執行的動作
1. 加載程序集
因為程序集是個比較大的概念,而這偏離了這篇文章的主題,請移步我的另一篇文章《程序集》。
2. 發現類型
FCL提供了許多API來獲取程序集中的類型,目前常用的API是Assembly中的ExportedTypes、DefinedTypes、GetType等,ExportedTypes屬性用來獲取公開方法即public類型,DefiedTypes屬性用來獲取所有類型,GetType方法獲取一個指定的類型。
var assembly = Assembly.Load(@"mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
int index=0;
Console.WriteLine("獲取程序集:{0} 中的ExportedTypes",assembly);
foreach (var type in assembly.ExportedTypes)
{
Console.WriteLine("{0}. {1}", ++index,type);
}
Console.WriteLine("ExportedTypes類型有{0}個",assembly.ExportedTypes.Count());
index = 0;
Console.WriteLine("獲取程序集:{0} 中的DefinedTypes", assembly);
foreach (var type in assembly.DefinedTypes)
{
Console.WriteLine("{0}. {1}", ++index, type);
}
Console.WriteLine("DefinedTypes類型有{0}個", assembly.ExportedTypes.Count());
3. 構造類型實例
在FCL中提供了幾個構造類型實例的機制分散在
System.Activator
,System.AppDomain
,System.Reflection.ConstructorInfo
中,如果查看源碼的話可以看到內部實現調用的都是Activator.CreateInstance
。
下面的例子演示實例的創建
public class Hello
{
public void Say()
{
Console.WriteLine("Hello Reflectioin!");
}
}
//出於演示的目的下面這句簡化了加載程序集的步驟
Type helloType = typeof(Hello);
//創建實例
var hello=Activator.CreateInstance(helloType);
4. 發現類型成員
在FCL中有反射提供了一個類型基類
System.Reflection.MemberInfo
,其派生類如下圖所示:
通常使用Type類型來發現成員類型如:Method,Filed,Property,Event等。
.Net 4中可以通過Type.GetTypeInfo擴展方法獲取TypeInfo
對象以便獲取更多功能,相比Type類型TypeInfo代價更高。
下面通過例子來說明如何發現成員:
成員類型描述:
* 一個事件OnSay
* 兩個字段,_name是private,Age是Public
* 一個Name屬性
* 三個方法,SayHello是Static public,Say是public,HaHa是private
using System;
namespace ReflectionAssembly
{
public class Hello
{
//事件
public event Action OnSay;
//私有字段
private string _name;
//共有字段
public string Age;
//屬性
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
//靜態方法
public static void SayHello()
{
Console.WriteLine("SayHello");
}
//實例方法
public void Say()
{
Console.WriteLine("Hello Reflectioin!");
}
//測試事件
public void TestEvent()
{
if (OnSay != null)
{
Console.WriteLine($"OnSay綁定了{OnSay.GetInvocationList().Length}個方法");
//遍歷執行綁定的事件
foreach (Action onSay in OnSay.GetInvocationList())
{
onSay();
}
}
}
//私有實例方法
void HaHa()
{
Console.WriteLine("HaHa");
}
}
}
因為MemberInfo是所有成員類型的基類,那么我們先看一下獲取所有的成員類型:
static void Main(string[] args)
{
var helloType = Assembly.Load("ReflectionAssembly").GetType("ReflectionAssembly.Hello");
var memberInfos = helloType.GetMembers();
foreach (var memberInfo in memberInfos)
{
Console.WriteLine($"成員類型:{memberInfo.MemberType}\t類型名稱:{memberInfo.Name}");
}
Console.WriteLine($"類型{helloType.FullName}共{memberInfos.Count()}個成員");
Console.Read();
}
//運行結果如下:
成員類型:Method 類型名稱:add_OnSay
成員類型:Method 類型名稱:remove_OnSay
成員類型:Method 類型名稱:get_Name
成員類型:Method 類型名稱:set_Name
成員類型:Method 類型名稱:SayHello
成員類型:Method 類型名稱:Say
成員類型:Method 類型名稱:ToString
成員類型:Method 類型名稱:Equals
成員類型:Method 類型名稱:GetHashCode
成員類型:Method 類型名稱:GetType
成員類型:Constructor 類型名稱:.ctor
成員類型:Property 類型名稱:Name
成員類型:Event 類型名稱:OnSay
成員類型:Field 類型名稱:Age
類型ReflectionAssembly.Hello共14個成員
Why?我們只定義了7個成員,結果卻顯示14個?
- 如果你了解Event類型那么可以忽略add_OnSay,remove_OnSay。《事件和委托學習總結》
- 如果你了解Property那么可以忽略get_Name,set_Name。
- 如果你了解Object那么可以忽略ToString,Equals,GetHasCode,GetType。(所有的引用類型都繼承Object)
- 如果你知道默認構造函數那么可以忽略.ctor。
- 現在剩下的
OnSay
,Age
,Name
,Say
,SayHello
5個類是我們定義的,還有2個_name
,Haha
沒有在上面運行結果中顯示,我們發現這個成員有個共同點是:他們都是私有成員
。
如何獲取私有成員
我們將代碼稍作改動看看效果:
var memberInfos = helloType.GetMembers();
//上面這行做如下改動
var memberInfos = helloType.GetMembers(BindingFlags.NonPublic //獲取private成員
|BindingFlags.Static //獲取static成員
|BindingFlags.Instance //獲取實例成員
|BindingFlags.Public //獲取public成員
);
//得到如下結果:
成員類型:Method 類型名稱:add_OnSay
成員類型:Method 類型名稱:remove_OnSay
成員類型:Method 類型名稱:get_Name
成員類型:Method 類型名稱:set_Name
成員類型:Method 類型名稱:SayHello
成員類型:Method 類型名稱:Say
成員類型:Method 類型名稱:HaHa
成員類型:Method 類型名稱:ToString
成員類型:Method 類型名稱:Equals
成員類型:Method 類型名稱:GetHashCode
成員類型:Method 類型名稱:GetType
成員類型:Method 類型名稱:Finalize
成員類型:Method 類型名稱:MemberwiseClone
成員類型:Constructor 類型名稱:.ctor
成員類型:Property 類型名稱:Name
成員類型:Event 類型名稱:OnSay
成員類型:Field 類型名稱:OnSay
成員類型:Field 類型名稱:_name
成員類型:Field 類型名稱:Age
類型ReflectionAssembly.Hello共19個成員
這里我們之關心我們定義SayHello,Say,HaHa,Name,OnSay,_name,Age的7個成員是否被列出來。
GetMembers,FindMembers內部實現
Type類型同時也提供FindMembers方法來獲取成員類型,如果觀察其內部實現會發現它們僅僅是一個包裝方法,
它們通過包裝GetMethods,GetFields,GetProperties,GetEvents,GetConstructors方法的實現來獲取所有的成員信息。
以下方法均提供了多個重載方法
發現字段(FildInfo)
helloType.GetFields();
發現屬性(PropertyInfo)
helloType.GetProperties();
發現方法(MethodInfo)
helloType.GetMethods();
發現構造器(ConstructorInfo)
helloType.GetConstructors();
發現事件(EventInfo)
helloType.GetEvents();
5. 執行對類的操作
調用字段
字段類型通過GetValue
,SetValue
方法對來操作
//實例化Hello
var hello = Activator.CreateInstance(helloType);
//調用private字段
var _name = helloType.GetField("_name",BindingFlags.NonPublic|BindingFlags.Instance);
_name.SetValue(hello, "guodf");
Console.WriteLine(_name.GetValue(hello));
//調用public字段
var age = helloType.GetField("Age");
age.SetValue(hello, "16");
Console.WriteLine(age.GetValue(hello));
調用屬性
屬性類型通過get_**
,set_**
方法來操作
//實例化Hello
var hello = Activator.CreateInstance(helloType);
//調用屬性
var name = helloType.GetProperty("Name");
name.SetValue(hello, "guodf test");
Console.WriteLine($"_name:{_name.GetValue(hello)}\tName:{name.GetValue(hello)}");
調用方法
方法通過Invoke執行方法
//調用實例方法
var say = helloType.GetMethod("Say");
say.Invoke(hello,null);
//調用private實例方法
var haha = helloType.GetMethod("HaHa", BindingFlags.Instance | BindingFlags.NonPublic);
haha.Invoke(hello, null);
調用事件
//調用事件
var testEvent = helloType.GetMethod("TestEvent");
var onSay = helloType.GetEvent("OnSay");
Action event1 = () => { Console.WriteLine("event1"); };
Action event2 = () => { Console.WriteLine("event2"); };
//綁定2個方法
onSay.AddEventHandler(hello, event1);
onSay.AddEventHandler(hello, event2);
testEvent.Invoke(hello,null);
//移除一個方法
onSay.RemoveEventHandler(hello, event1);
testEvent.Invoke(hello,null);
調用靜態方法
var sayHello=hello.GetMethod("SayHello");
sayHello.Invoke(null, null);
隱式反射和顯式反射
C#中有隱式轉換和顯式轉換得概念,通常派生類轉換為基類型被稱為隱私轉換,因為可以直接將派生類型賦值給基類型;反之稱為顯示轉換。
那么在反射得使用過程中,我通常使用兩種實現方式來使用反射對象:一種基於接口的編程方式,另一種則是完全的字符串查找方式。所以我將基於接口的方式稱為顯式反射,這種做法的好處是編程期間我們可以直接使用類型的方法;而另一種基於字符串找好的方式我稱它為隱式反射,因為在使用過程中無論得到那種成員類型都是通過字符串查找實現的。
反射的優缺點
優點
1. 動態加載,按需加載
2. 解耦
缺點
1. 無編譯期類型安全檢查
2. 性能低
小結
反射一種技術,這種技術可以幫助我們實現一些看起來很酷的編程設計,但這種技術並不完美,它犧牲了效率換來了靈活性。至於這種犧牲的價值當然是仁者見仁智者見智。