c# 反射專題—————— 介紹一下是什麼是反射[ 一]

2022-07-11 06:50:24

前言

為什麼有反射這個系列,這個系列後,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, 只是個人用通俗的語言整理一下。