理解C# 核心概念 – C# 模組(module)
模組,準確的來說,並不是C#的概念,而是微軟的執行時環境CLR的概念。想要學好C#斷然不能只是會使用VS之類的IDE寫寫程式碼然後編譯執行,很多相關的概念我們也需要去了解。
這一節,我們就簡單討論下C#模組。
其實C#模組,我們幾乎每天都需要接觸到(如果我們每天都需要寫C#程式碼並且進行編譯的話),每次我們對一個C#類(class)進行編譯都會生成模組。
csc編譯獲得模組
下面,我們以經典的hello world為例。
- 建立一個Program.cs檔案,內容只是簡單的向控制檯輸出hello world:
public sealed class Program{ public static void Main() { System.Console.WriteLine("hello world"); } }
- 使用csc.exe 對檔案進行編譯:
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs
如果以上命令執行成功的話,我們會在原始檔的資料夾中得到一個Program.exe的檔案。這個Program.exe 就是一個標準的PE32或者PE32+的檔案(PE32+對應的是64位,這裡沒有PE64),也就是一個C#模組。
在繼續講解C#模組之前, 我們先來看看上面這個編譯命令,這個命令還是有點意思的。
/out:Program.exe 告訴了編譯器我們最後需要生成的是一個exe檔案。
/t:exe 看起來和上一個out引數似乎有些重複,這也是這個引數比較迷惑人的地方,t其實代表的是target,exe其實只是指代了沒有圖形介面的應用程式。如果是我們平常使用的比如迅雷之類的應用那麼使用的就應該是winexe。這種型別是具有圖形介面的。
/r:MSCorLib.dll,r這裡是reference,是為了宣告編譯Program.cs我們需要使用到定義在MSCorLib.dll 中的某些型別(在我們的例子中就是Console)。
Response檔案
有心的朋友可能會發現,其實我們並不需要宣告MSCorLib.dll的引用,直接使用如下命令,也能夠編譯成功。
csc.exe /out:Program.exe /t:exe Program.cs
事實上確實如此,為了方便進行編譯,而不需要每次都輸入一串引數,csc允許我們使用Response檔案來提供引數。
因此我們可以把剛剛使用到的三個引數全部放到response 檔案中並儲存為Program.rsp,這個檔案內容如下:
/out: Program.exe
/t:exe
/r:MSCorLib.dll
然後執行如下命令,也可以得到同樣的結果。
csc.exe @Program.rsp Program.cs
引數覆蓋規則
聰明的朋友肯定又發現了,可是我們之前並沒有定義response檔案,為什麼我們還是可以不指定引用。這個原因就在於,csc自帶了一個全域性預設的response檔案。我們可以在csc的安裝目錄中找到這個csc.rsp檔案。而這個csc.rsp檔案就包含了許多系統自帶的常用的引用。因此我們並不需要去宣告這些引用。
老白使用的是VS2019安裝的版本,因此csc.exe檔案可以在“C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Roslyn\”路徑中找到,同樣的,這個資料夾也包含了csc.rsp檔案。
既然存在多個地方可以進行配置,那必然會存在優先順序的問題。想必聰明的朋友已經想到了,csc採用的是以下順序獲取引數,一旦獲得需要的引數就不再向下查詢:
- 命令列中帶的引數
- 本地傳入命令列的response檔案中的引數
- 全域性的csc.rsp中的引數
模組內容
通過csc編譯出來的模組,需要使用CLR進行執行。那麼一個模組是怎麼讓CLR知道應該怎麼進行處理的呢?這就需要我們看看模組到底包含了哪些資訊了。
PE32/PE32+的標頭檔案
標頭檔案這個東西,名字很有意思,其實也非常準確,一般來說我們看到一個人的“頭”就能知道這個人是誰(who),大致的年齡(how old,簡稱how),雖然不準確但是靈魂三問已經完成兩問了。
同樣的,PE32/PE32+標頭檔案也給我們提供了這些資訊。標頭檔案裡包含了PE檔案型別的資訊(who),我們可以知道這是一個GUI檔案還是一個普通DLL等等。此外也包含了檔案建立資訊(how old)。如果這個PE標頭檔案使用了PE32的格式,那麼就可以在32位或者64位的Windows上執行。如果使用的是PE32+的格式,那麼只能在64位的Windows上執行。
CLR 標頭檔案
如果說PE32/PE32+標頭檔案沒什麼用途的話(事實上也確實如此,CLR大部分情況下並不使用PE標頭檔案),那麼CLR標頭檔案則是包含了CLR執行此檔案的必要資訊。幾個主要的資訊包括了,所需要的CLR版本,模組的入口函式,模組的metadata資料位置和大小等等。
元資料(metadata)
主要包含了兩部分,一部分是這個模組本身所包含的型別(type)和成員(member)。另外一部分描述了這個模組會引用到型別和成員資訊。
IL 程式碼
整個模組的核心部分,是編譯器編譯C#原始碼所生成的中間程式碼。在執行時,IL程式碼會被CLR編譯成cpu指令。
這就是一個模組所包含的4個部分。這裡比較重要的一點是這四個部分都是相互耦合在一起的,這樣就能夠保證元資料和IL程式碼之間不會出現不同步的問題。
此外,可能有很多同學都會有困惑,為啥已經有了IL程式碼了,其實就能夠知道這個模組本身所包含的型別和成員也能之後引用了哪些其他模組,為啥還需要元資料呢?我的理解也不一定準確,但是很大一個作用就是GC可以通過閱讀元資料來很快的判斷哪些型別之間有引用,從而判斷是否需要進行GC。此外,我們編寫的原始碼,在進行編譯的時候,編譯器也能夠很快的通過元資料來判斷我們的原始碼是否合規。
好了,到此,老白就將如何(手動)產生一個模組以及模組的組成說完了。如果有什麼問題,歡迎大家和老白一起探討。
碼字不易,如果覺得有用或者喜歡老白的內容,歡迎點贊收藏關注。