為什麼有反射這個系列,這個系列後,asp net 將會進入深入篇,如果沒有這個反射系列,那麼asp net的原始碼,看了可能會覺得頭暈,裡面的依賴注入包括框架原始碼是大量的反射。
下面是官方檔案的介紹:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/reflection
說的比較繞,反射就是用來動態建立物件的。
那麼什麼是動態建立物件? 動態建立物件就是執行時建立物件。
那麼為什麼需要動態建立物件呢?
可以思考一下,我們寫程式碼的時候為什麼需要動態建立?
這裡我舉一個例子。
比如說,eventbus,通過不同的字串反射成不同的事件。
可能有人沒怎麼接觸這個eventbus,再舉個例子。
有一個api,使用者可以傳入動物的名字和該動物的一些屬性,那麼當我們拿到這些字串的時候,我們在內部根據動物的名字和屬性建立響應的物件。
反射動態建立物件:
static void Main(string[] args)
{
Assembly assembly = Assembly.GetExecutingAssembly(); // 獲取當前程式集
dynamic obj = assembly.CreateInstance("ConsoleApp7.Cat");
Console.WriteLine(obj);
}
可以看到,我們通過反射建立一個物件。
那麼為什麼反射能建立一個物件呢? 它的原理是什麼呢?
在https://docs.microsoft.com/zh-cn/dotnet/framework/reflection-and-codedom/viewing-type-information 文中寫道:
當反射提出請求時,公共語言執行時為已載入的型別建立 Type 。 可使用 Type 物件的方法、欄位、屬性和巢狀類來查詢該型別的任何資訊。
也就是說在執行的時候,會為載入的型別,建立Type,那麼如何獲取Type 呢?
Type catType = Type.GetType("ConsoleApp7.Cat");
那麼這裡原理也清晰了,原來在.net 執行的時候會為載入的型別建立Type,通過Type 就能建立範例。
那麼Type 裡面有什麼呢? 首先肯定有建構函式吧,不然怎麼建立範例呢? 那麼還有什麼呢?
上文也提及到了裡面有方法、欄位、屬性、巢狀類等用來描述這個型別的資訊。
舉個例子:比如說我這個Cat 類吧, 在載入的時候會建立Cat的Type,那麼這個Type 裡面就存有我這個Cat的方法、欄位、屬性等,這些可以統稱為後設資料。
後設資料也就是metadata:
可以看下下面這篇介紹:
https://baike.baidu.com/item/後設資料/1946090?fr=aladdin
後設資料(Metadata),又稱中介資料、中繼資料,為描述資料的資料(data about data),主要是描述資料屬性(property)的資訊,用來支援如指示儲存位置、歷史資料、資源查詢、檔案記錄等功能。
這就是這個Type 就是對載入型別的描述了,有了它那麼可以做事情。
例如獲取屬性,獲取方法等。
舉個例子:
Type catType = Type.GetType("ConsoleApp7.Cat");
var cat = Activator.CreateInstance(catType);
這個時候有人就問了,為什麼c# 不設計成: catType.CreateInstance();
這樣獲取不是更加簡單嗎? 這就是c# 的優雅的地方。 Type 是僅僅對型別的描述,這樣 Type 就不會隨著擴充套件變得臃腫。不談這個領域驅動篇馬上開始了,這裡面會介紹。
那麼這裡有一個問題了,其實我們反射建立物件也就是為了呼叫裡面方法,那麼這個怎麼做呢?
比如說呼叫裡面的Cat的Hi 方法?
public class Cat
{
public void Hi()
{
Console.WriteLine("hi master!");
}
}
然後這樣寫:
static void Main(string[] args)
{
Type catType = Type.GetType("ConsoleApp7.Cat");
var cat = (Cat)Activator.CreateInstance(catType);
cat.Hi();
Console.ReadKey();
}
這樣寫會執行正常嗎? 那肯定能執行正常。
那麼我們為什麼一般不這樣寫?這裡就有一個問題,我們反射的目的是什麼?
是為了動態載入,動態載入是為了動態建立物件? 這顯然不是,是為了動態能夠執行某段程式碼,也就是動態執行。
既然我們在執行的時候才知道型別,那麼我們是如何能保證知道我們要呼叫的方法呢?
所以一般這樣寫:
static void Main(string[] args)
{
Type catType = Type.GetType("ConsoleApp7.Cat");
var cat = (Cat)Activator.CreateInstance(catType);
var hiMethod = catType.GetMethod("Hi");
hiMethod.Invoke(cat, null);
Console.ReadKey();
}
這裡不要看我寫的是固定ConsoleApp7.Cat 和 Hi,到了真正寫程式碼都是傳入引數進去的。
一樣的能夠執行。
從上面可以看出,其實在反射的眼裡,方法也就是物件,通過把Hi 這個方法當成了himethod 這個物件。
可能在c# 中表現的不是很明顯,在js 語言中表現的非常明顯,方法就是一個物件。
看起來反射挺好用的啊,那麼現在反射的優點就是動態載入,那麼反射的缺點有嗎?
反射的缺點也很明顯,那就是執行更慢,為什麼會執行更慢呢? 那是因為反射是幾乎是邊解釋邊執行的。
為什麼反射會邊解釋邊執行呢? 因為他做不到先編譯再執行,微軟在新的c#版本中做了優化,如何能避免反射,這個後續說明。
下面測試一下效能:
cat 中加入方法:
static void Main(string[] args)
{
Type catType = Type.GetType("ConsoleApp7.Cat");
var cat = Activator.CreateInstance(catType);
var hiMethod = catType.GetMethod("Hi");
Console.WriteLine("反射的執行時間");
Stopwatch stopwatch = Stopwatch.StartNew();
for (var i =0; i<9999999; i++)
{
hiMethod.Invoke(cat, null);
}
stopwatch.Stop();
var elapsed = stopwatch.Elapsed;
Console.WriteLine(elapsed.Milliseconds);
Console.WriteLine("正常建立物件的時間");
Cat cat2 = new Cat();
Stopwatch stopwatch2 = Stopwatch.StartNew();
for (var i = 0; i < 9999999; i++)
{
cat2.Hi();
}
stopwatch2.Stop();
var elapsed2 = stopwatch2.Elapsed;
Console.WriteLine(elapsed2.Milliseconds);
Console.ReadKey();
}
結果:
可以看到相差非常遠。
那麼既然效能相差這麼遠,為什麼我們還要用反射呢?
這個問題很簡單,很多人喜歡拿效能說事,簡單的說就是我們做工程的,考慮的是工程角度,效能是是否能滿足需求的指標,反射大多數場景還是滿足需求的。
那麼反射是否可以優化?
可以。 我沒有優化過,網上有很多文章,沒有遇到過這種場景。
這一節只是介紹一下什麼是反射,和反射的優缺點,這是一個系列,所以後面大多數是介紹一些反射的api呼叫 和 原理。以上為個人整理,如有錯誤望請指點。
很大一部分參考於官方檔案: https://docs.microsoft.com/zh-cn/dotnet/framework/reflection-and-codedom/reflection, 只是個人用通俗的語言整理一下。