文章目錄
- 6.3.1 BCL——基底類別庫
- 6.3.2 FCL——架構類庫
- 6.6.1 程式集概述
- 6.6.2 運行程式集
.NET架構
本書是一本講解.NET技術的書籍,目標讀者群也是在.NET架構(.NET Framework)下進行開發的程式員,因此我們無法迴避的問題就是:什麼是.NET架構?它包含了哪些內容?為開發程式提供了哪些支援?很多朋友對這類個問題的第一反應可能是.NET架構所提供的龐大類庫及編寫代碼所採用的C#語言,實際上遠不止這些。
要描述.NET架構,自然會遇到與其相關的一系列專業的技術術語和縮寫,相信大家已經見到過許多了,比如:CLI、CIL、CTS、CLS、CLR、JIT、BCL、FCL、Module、Assembly 等,足以讓很多人一頭霧水、望而卻步。筆者不會像字典一樣按首字母排序對術語進行逐一解釋,因為這樣還是難以理解。我們還是從大家最熟悉的東西開始吧!
6.1 引子
設想一下:編寫下面這樣一個最簡單的顯示“Hello, World!”的控制台程式,並將該程式運行起來需要哪幾個步驟呢?
using System;
class Program {
static void Main(string[] args) {
string text = "hello, world!";
Console.WriteLine(text);
}
}
這些步驟包括:開啟Visual Studio,建立一個C#控制台應用程式項目(在這裡將它命名為ConsoleApp),編寫代碼,編譯器然後運行。雖然這樣的程式誰都會寫,但是再多進行一下思考就會發現,儘管是一個很小的程式,但已經引入了.NET架構的幾個重要方面。
如果建立一個VB.NET類型的項目,實現和上面C#項目完全一樣的功能,那麼編譯後產生的檔案有什麼區別?
編寫控制台應用程式,將字元輸出到螢幕,需要調用Console.WriteLine()方法。這個Console類型從何而來呢?
產生的檔案在系統中是如何運行起來的?其機制和使用傳統VC++產生的可執行檔是否相同?
其實,上面每一個問題的答案都包含.NET架構所提供的支援,這裡將它分為三個部分:
- 對於編譯後產生的檔案格式和內容,.NET中存在著諸多規範。符合這些規範的程式語言,也叫做面向.NET的語言。編譯後產生的檔案都可以在.NET運行時下執行,這就是大家所熟知的.NET多語言支援。
- 在開發階段,.NET提供了一個龐大的類庫,支援開發人員快速開發各種應用程式,也支援程式語言設計者開發其語言編譯器。
- 在程式執行階段,.NET提供了一個程式運行時的環境,這個運行時環境協助我們管理記憶體、Just-In-Time 編譯程式、進行安全檢查、執行記憶體回收等。
接下來就針對上述內容開始為大家詳細講述。
6.2 CIL——通用中間語言
首先要瞭解的就是C#程式源碼在編譯之後會得到什麼樣的一個檔案。大家知道,過去使用VC++產生的可執行檔,經過先行編譯、編譯、彙編、連結幾個步驟後,最終產生的可執行檔中就已經包含了處理器的本地代碼(Native Code),支援它啟動並執行只是作業系統和本地的機器指令集。那麼採用C#編譯器產生的檔案又是什麼呢?現在需要引入程式集這個概念:在.NET架構下,類似C#這樣的進階語言經過編譯後產生的結果檔案被稱做程式集,其尾碼名是.dll(類庫)或.exe(可執行程式)。在引入這個概念之前,前面(上一節)提到程式集時,都是用“檔案”這個詞來描述的。
程式集的定義只是給編譯後產生的檔案一個稍微正式一點的名稱,對於解釋“它是由什麼構成的”這個問題並沒有太大的協助。為了進一步瞭解程式集,我們再來做一個實驗,使用VB.NET建立一個控制台應用程式項目(ConsoleAppVB),並產生一個程式集,代碼功能和上面用C#建立的項目是一樣的的。
Module Program
Sub Main()
Dim text AsString = "hello, world !"
Console.WriteLine(text)
EndSub
EndModule
現在,需要一個工具來查看這個程式集的內容,並且與C#項目產生的程式集進行對比。還好,微軟已經提供了一個利器——IL DASM(IL Disassembler,IL反組譯工具)來協助開發人員查看程式集的資訊。如果安裝了Visual Studio,IL DASM將會隨同Visual Studio一起安裝。依次選擇開始菜單→ Microsoft Visual Studio 2010 → Microsoft Windows SDK Tools →IL 反組譯工具(IL DASM)可以啟動IL DASM。
開啟IL DASM後選擇VB.NET項目產生的ConsoleAppVB.exe,可以看到6-1所示的介面。
圖6-1 IL DASM 運行介面
這部分內容很多,會在下一章“程式集”中進行專門講述,,這裡暫且略過。展開圖6-1中的ConsoleAppVB.Program類型,在Main()方法上雙擊,會彈出另外一個視窗,顯示圖6-2中的代碼,看上去有點像組合語言。在這裡可以看到熟悉的string text變數聲明及“hello, world !”。
圖6-2 方法體的CIL語言描述(VB.NET)
接下來再開啟C#項目產生的ConsoleApp.exe,進行同樣的操作,在開啟Main()方法後會發現其中的代碼與圖6-2中幾乎完全一樣,6-3所示
圖6-3方法體的CIL語言描述(C#)
至此,可以得到一個初步的推斷:不管是VB.NET還是是C#,編譯之後的程式集都能夠用IL DASM開啟,因此它們產生的程式集的格式都是相同的;當程式所實現的功能相同時,程式集所包含的CIL代碼也是類似的。
現在對上面程式集中所包含的類似彙編的語言做一下介紹,即是本區段標頭中的CIL(Common Intermediate Language,通用中間語言)。CIL最初是隨著.NET由微軟一起發布的,因此之前也叫做MSIL(Microsoft Intermediate Language),後來進行了標準化,之後便被稱做CIL。在一些書或文章中,CIL也會簡寫為IL,其實都是指同樣的東西。為了避免混淆,本書統一用CIL這個縮寫。
我們可以將上面的過程用圖6-4來表示出來。
圖6-4 來源程式編譯為了程式集
接下來再深入地分析一下,通用中間語言這個術語到底包含了哪幾層含義。
- 公用。因為不論是C#語言也好,VB.NET語言也好,C++/CLI語言也好,甚至是重新開發的一套以自己的名字縮寫命名的語言,只要它期望啟動並執行目標平台是.NET,在經過相應的編譯器編譯之後,所產生的程式集就是由CIL語言代碼描述的。
- 中間。這個詞也是大有深意,為什麼不叫公用機器語言(Common Machine Language),或者公用本地語言(Common Native Language)?因為這種語言只是比我們使用的進階語言,比如C#低級一點,並不是CPU可以直接執行的本地機器語言。這種語言還需要.NET運行時(.Net runtime)環境的支援,在執行之前,進行一個被稱為Just-in-time(即時)的二次編譯過程,才能轉變成電腦可以識別的指令。關於.NET運行時,以及詳細過程後面再介紹,現在只要知道,這個檔案所包含的CIL代碼並非機器可以直接執行的指令代碼。
- 語言。CIL不過是一種程式語言,只不過相對於C#來說,它是一種更低級語言。從圖6-2 的代碼中,已經可以看到,CIL是一種基於堆棧的語言,同時,它提供了class、interface、繼承、多態等諸多物件導向的語言特性,因此它又是完全物件導向的語言。如果願意,甚至可以直接編寫CIL代碼,並且使用CIL的編譯工具IL ASM(IL Assembler,IL組譯工具)來對它進行編譯。只不過,和大多數低級語言一樣,這種方式會使開發效率會變得很低。這裡注意區別一下IL ASM和IL DASM,它們的拼字是不同的。
為了加深一下印象,我們來做一個實驗:編寫一段簡單的CIL代碼,並且使用IL ASM工具對其進行編譯,得到和前面一樣的ConsoleApp.exe程式。
1)開啟記事本程式,輸入下面的代碼,然後將其儲存在D:\ConsoleApp.il。
.assembly extern mscorlib{}
.assembly ConsoleApp{}
.module ConsoleApp.exe
.class public auto ansi Program extends System.Object
{
.method public static void Main()
{
.entrypoint
nop
ldstr "Hello, World!"
call void [mscorlib]System.Console::WriteLine(string)
nop
ret
}
}
2)開啟Visual Studio 2010命令列工具,輸入:
D:\>ilasm ConsoleApp.il
3)成功後會看到ConsoleApp.exe程式,它的執行結果和上面用C#編寫的完全一樣。
由於程式集是由CIL語言所描述的,因此CIL也叫做程式集語言(Assembly Language)。又因為.NET程式集需要由.NET運行時載入才能運行,可以視其為由.NET運行時進行管理的,所以CIL代碼也叫做Managed 程式碼(Managed Code)。相對的,不需要.NET運行時就可以執行的代碼就叫做Unmanaged 程式碼(Unmanaged Code)。
好了,已經知道了CIL的存在,從現在開始,最好在頭腦裡建立起兩個模型或兩種視角:一種是基於C#或其他進階語言的來源程式的視角,一種是基於CIL中繼語言的程式集視角。C#來源程式在被編譯為程式集以後,就獨立於C#,因此程式集可以由其他種類的語言所調用;同時,因為程式集並沒有包含本地機器的指令,所以它與具體的機器類型也分隔開了,可以被裝有.NET架構的任何機器運行。
6.3 BCL和FCL6.3.1 BCL——基底類別庫
我們先來看一個有意思的現象:再次開啟前面建立的C#控制台項目(ConsoleApp),然後在解決方案面板下開啟“引用”檔案夾,如果用的是Visual Studio 2010,並且面向的目標框架是.NET 4.0版本,那麼將會看到6-5所示的這些引用。
圖6-5 解決方案中的“引用”檔案夾
在建立項目時並沒有做任何額外的操作,那麼這些引用顯然是在建立項目時自動添加的。為了方便初學者,這裡稍微解釋一下:要使用(實際上筆者覺得Consume這個詞表達的更貼切)其他開發人員所設計的類型,就需要在項目中將該類型所在的程式集引用進來。現在看到的這些程式集引用,都是微軟認為很常用的,幾乎是每個項目都會使用到的,所以在建立項目時自動添加了進來,免得開發人員再手動進行添加。
但是在這裡這些引用不利於我們理解一些內容,所以我們把這些引用全部刪除掉,6-6所示,然後再次編譯器。
圖6-6 刪除掉所有的項目引用
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp {
classProgram {
staticvoid Main(string[] args) {
string text = "Hello, world!";
Console.WriteLine(text);
}
}
}
可能有人會認為,在刪掉這些引用之後,編譯器將會毫不客氣地提示編譯錯誤:未能找到類型或命名空間“System”(是否缺少using指令或程式集引用?)。可實際上,當編譯並運行上面的代碼時,程式會正確無誤地執行。這是因為我們已經刪掉了所有引用的程式集,只定義了一個Program類型,並沒有定義Console類型,所以此時要面對的第一個問題就是:Console類型從哪裡來?
Visual Studio提供了一個快捷的辦法使我們可以快速查看類型:將游標定位在Console上,然後按下鍵盤上的F12,就可以看到Console的類型定義。在Console類型定義的最上方,可以看到它所在的程式集地址:C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll。
#region 程式集 mscorlib.dll, v4.0.30319
// C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
#endregion
using System.IO;
using System.Runtime.ConstrainedExecution;
using System.Security;
using System.Text;
namespace System {
public static class Console {
// 中間略
}
}
可以看到Console類型來自於mscorlib.dll這個程式集。從上面的實驗可以看出,不管我們是否引用mscorlib.dll程式集,它總是會自動引用進來。這個程式集中所包含的類庫,即是本區段標頭中的BCL(Base Class Library,基底類別庫)。從名字就可以看出來,這個類庫包含的都是些最基本的類型,其本身已經與CIL語言融為一提了,為CIL語言提供基礎的編程支援,以至於該類庫已經成為了CLI標準的一部分(後面會介紹,因此也可以說BCL中的類型就是CIL語言的類型,所有面向CIL的語言都能夠使用它們。我們可以使用物件瀏覽器(Visual Studio菜單→視圖→物件瀏覽器)來查看mscorlib.dll程式集中都包含了哪些命名空間和類型,6-7所示。
圖6-7 mscorlib.dll中包含的命名空間
可以看到該程式集下包含的主要是System命名空間,稍微細心一點的讀者會發現,在建立項目的時候,還包含了System.dll程式集,並且其中所包含的類型與mscorlib中的類型十分相似。
圖6-8 System 程式集
圖6-9 System.dll中包含的命名空間
這又是怎麼回事呢?實際上,只要點開System命名空間就會發現,mscorlib.dll的System命名空間下面定義的類型和System.dll的System命名空間下面定義的類型完全不同,它們之間並沒有衝突之處。
現在就明白了:BCL提供了像Console這樣的類型來支援開發人員編寫類似控制台這樣的程式。
既然已經思考了這麼多,不妨再深入一下,思考這樣一個問題:寫下的這條語句string text = “hello, world !”,其中的string從哪裡來?從直覺來看,string在Visual Studio中以深藍色呈現,屬於C#的關鍵字,那麼它應該是C#提供的內建類型。可是,當我們將游標移動到string上並按下F12時,轉到string的定義時,看到的卻是下面這樣的內容:
#region 程式集 mscorlib.dll, v4.0.30319
// C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll
#endregion
using System.Collections;
using System.Collections.Generic;
// 為了節約篇幅,省略了一些using
namespace System {
public sealed class String : IComparable, ICloneable, IConvertible, IComparable<string>, IEnumerable<char>, IEnumerable, IEquatable<string> {
// 省略定義
}
}
注意最上方的程式集地址,再次看到了mscorlib.dll,並且String類型與Console類型一樣,同位於System命名空間下。由此可見,C#的關鍵字string,不過是BCL中System.String類型的一個別名而已。類似地,VB.NET中的String關鍵字也是BCL中的System.String類型的別名。因此,在.NET架構中,語言從本質上來說沒有太大的區別,更多的區別是在文法方面。從上面的例子也可以看出,C#和VB.NET的很多語言能力並不是自己的,而是從CIL“借”過來的這樣做也保證了在不同語言中相應類型的行為是一致的。
表6-1列出了幾個典型的,不同語言關鍵字與CIL類型的對應關係。筆者覺得理解重於記憶,所以這裡只列出了幾個。要瞭解其他基礎類型時,只要將游標移動到類型上,然後再按下F12鍵就可以了。
表6-1不同語言關鍵字與CIL類型的對應關係
| CIL 類型 |
C# 關鍵字 |
VB.NET關鍵字 |
| System.Byte |
byte |
Byte |
| Sytem.Int16 |
short |
Short |
| System.Int64 |
long |
Long |
從表6-1可以看出,.NET同時也對語言開發人員提供支援.如你需要設計一款語言,那麼在開發編譯器時將語言的關鍵字映射為CIL中的類型就可以了,也就是說,對自己語言中的一些特殊符號(關鍵字)進行映射處理,就好像C#中的關鍵字int和string一樣。
大家可能聽說過這樣一種特殊的類型——基元類型(Primitive Type)。實際上,講到這裡大家應該已經明白了,那些由編譯器直接支援,將語言本身的關鍵字類型轉換為CIL類型的,就叫做基元類型。顯然,上面的byte、int、string都是基元類型。而C#中並沒有一個關鍵字去映射Console,所以我們認為Console只是普通的類類型(Class Type)。
6.3.2 FCL——架構類庫
作為一名.NET程式員,每天都要打交道的就是FCL了(Framework Class Library,架構類庫)。在上一節中介紹了BCL,它是FCL的一個子集。BCL中包含了與編譯器及CIL語言關係緊密的核心類型,以及常見開發工作單位中都會使用到的類型。而FCL包含的內容極多,僅服務於一種應用情境的子類庫就足夠寫一本書了,這裡僅簡單對它進行介紹。
從功能上來看,可以將FCL架構類庫劃分成以下幾層。
- 最內一層,由BCL的大部分組成,主要作用是對.NET架構、.NET運行時及CIL語言本身進行支援,例如基元類型、集合類型、線程處理、應用程式定義域、運行時、安全性、互操作等。
- 中間一層,包含了對作業系統功能的封裝,例如檔案系統、網路連接、圖形映像、XML操作等。
- 最外一層,包含各種類型的應用程式,例如Windows Forms、Asp.NET、WPF、WCF、WF等。
6.4 CTS——公用類型系統
假設要開發一套新的語言,這種語言和C#或VB.NET一樣,在編譯後也能夠產生CIL代碼,也可以在.NET環境下運行,那麼首先需要什麼呢?
根據6.2節所講述的內容我們知道,要開發的新語言相當於CIL的進階語言版本,所以實際上要做什麼並不是由新語言決定的,而是由CIL來決定的。因此,需要一套CIL的定義、規則或標準。這套規則定義了我們的語言可以做什麼,不可以做什麼,具有哪些特性。這套規則就稱作CTS(Common Type System,公用類型系統)。任何滿足了這套規則的進階語言就可以稱為面向.NET架構的語言。C#和VB.NET不過是微軟自己開發的一套符合了CTS的語言,實際上還有很多的組織或團體,也開發出了這樣的語言,比如Delphi.Net、FORTRAN等。
那麼CTS具體包括哪些內容呢?在回答這個問題之前我們需要弄清楚一個概念。還是通過一段C#代碼來說明,先看下面幾行代碼:
public class Book {
// 省略實現
}
Book item1 = new Book();
Book item2 = new Book();
對於以上代碼,通常是這麼描述的:定義了一個Book類,並且建立了兩個Book類的執行個體item1、item2。實際上這隻包含了兩層含義如表6-2所示。
表6-2 類、類的執行個體
| 類 |
Book |
| 類的執行個體 |
item1,item2 |
再思考一下就會發現,還有一個更高的層面,那就是Book這個類的類型,我們稱之為類類型(Class Type),因此上表可以改成如表6-3所示。
表6-3 類類型、類、類的執行個體
| 類類型 |
class |
| 類 |
Book |
| 類的執行個體 |
item1,item2 |
類似的,還有枚舉類型(Enum Type)、結構類型((Struct Type)等。現在大家應該明白這裡要表達的意思了,CTS規定了可以在語言中定義諸如類、結構、委託等類型,這些規則定義了語言中更高層次的內容。因此,在C#這個具體的語言實現中,我們才可以去定義類類型(Class Type)或者結構類型(Struct Type)等。
同樣,可以在Book類中定義一個欄位name並提供一個方法ShowName()。實際上,這些也是CTS定義的,它規範了類型中可以包含欄位(filed)、屬性(property)、方法(method)、事件(event)等。
除了定義各種類型外,CTS還規定了各種訪問性,比如Private、Public、Family(C#中為Protected)、Assembly(C#中為internal)、Family and assembly(C#中沒有提供實現)、Family or assembly(C#中為protected internal)。
CTS還定義了一些約束,例如,所有類型都隱式地繼承自System.Object類型,所有類型都只能繼承自一個基類。從CTS的名稱和公用類型系統可以看出,不僅C#語言要滿足這些約束,所有面向.NET的語言都需要滿足這些約束。眾所周知,傳統C++是可以繼承自多個基類的。為了讓熟悉C++語言的開發人員也能在.NET架構上開發應用程式,微軟推出了面向.NET的C++/CLI語言(也叫託管C++),它就是符合CTS的C++改版語言,為了滿足CTS規範,它被限制為了只能繼承自一個基類。
關於上面內容有兩點需要特別說明:
1)C#並沒有提供Family and assembly的實現,C#中也沒有全域方法(Global Method)。換言之,C#只實現了CTS 的一部分功能。,也就是說,CTS規範了語言能夠實現的所有能力,但是符合CTS規範的具體語言實現不一定要實現CTS規範所定義的全部功能。
2)C++/CLI又被約束為只能繼承自一個基類,換言之,C++中的部分功能被刪除了。,就是說,任何語言要符合CTS,其中與CTS不相容的部分功能都要被捨棄。
顯然,由於CIL是.NET運行時所能理解的語言,因此它實現了CTS的全部功能。雖然它是一種低級語言,但是實際上,它所具有的功能更加完整。C#語言和CIL的關係,可以用圖6-10進行表示。
圖6-10 C#和CIL的關係
6.5 CLS——Common Language Specification
既然已經理解了CTS是一套語言的規則定義,就可以開發一套語言來符合CTS了。假設這個語言叫做N#,它所實現的CTS非常有限,僅實現了其中很少的一部分功能,它與CTS和C#語言的關係可能6-11所示。
圖6-11 C#、N#和CIL的關係
那麼現在就有一個問題:由C#編寫的程式集,能夠引用由N#編寫的程式集嗎?答案顯然是不能,,雖然C#和N#同屬於CTS旗下,但是它們並沒有共通之處。因此,雖然單獨的N#或C#程式可以完美地在.NET架構下運行,但是它們之間卻無法相互引用。如果使用N#開發項目的開發人員本來就不希望其他語言類型的項目來引用他的項目倒也罷了,但是,如果N#項目期望其他語言類型的項目能夠對它進行引用,就需要N#中公開的類型和功能滿足C#語言的特性,即它們需要有共通之處。注意,這句話中有一個詞很重要,就是“公開的”(public)。N#中不公開的部分(private、internal、protected)是不受影響的,可以使用專屬的語言特性,因為這些不公開的部分本來就不允許外部進行訪問。因此, 如果N#想要被C#所理解和引用,它公開的部分就要滿足C#的一些規範,此時,它與CTS和C#語言的關係就會變成6-12所示。
圖6-12 C#、N#、CIL的關係
如果世界上僅有C#和N#兩種語言就好辦了,把它們共同的語言特性提取出來,然後要求所有公開的類型都滿足這些語言特性,這樣C#和N#程式集就可以相互引用了。可問題是:語言類型有上百種之多,並且.NET的設計目標是實現一個開放的平台,不僅現有的語言經過簡單修改就可以運行在.NET架構上,後續開發的新語言也可以,而新語言此時並不存在,如何提取出它的語言特性?因此又需要一套規範和標準來定義一些常見的、大多數語言都共有的語言特性。對於未來的新語言,只要它公開的部分能夠滿足這些規範,就能夠被其他語言的程式集所使用。這個規範就叫做CLS (Common Language Specification,Common Language Specification)。很明顯,CLS是CTS的一個子集。現在引入了CLS,圖6-12的關係圖就可以改成6-13所示。
圖6-13 語言、CLS、CIL的關係
如果利用C#開發的一個程式集的公開部分僅採用了CLS中的特性,那麼這個程式集就叫做CLS相容程式集(CLScompliant assembly)。顯然,對於上面提到的FCL架構類庫,其中的類型都符合CLS,僅有極個別類型的成員不符合CLS,這就保證了所有面向.NET的語言都可以使用架構類庫中的類型。
現在,讀者又會有一個疑問:上面幾段文字中反覆出現了一個詞———“語言特性”(language features),滿足CLS就是要求語言特性要一致,那麼什麼叫做語言特性?這裡給出幾個具體的語言特性:是否區分大小寫,標識符的命名規則如何,可以使用的基本類型有哪些,建構函式的調用方式(是否會調用基類建構函式),支援的存取修飾詞等。
那麼我們如何檢驗程式集是否符合CLS呢?.NET為我們提供了一個特性CLSCompliant,便於在編譯時間檢查程式集是否符合CLS。我們來看下面一個例子:
using System;
[assembly:CLSCompliant(true)]
public class CLSTest {
public string name;
// 警告:僅大小寫不同的標識符“CLSTest.Name()”不符合 CLS 規範
public string Name() {
return "";
}
// 警告:“CLSTest.GetValue()”的傳回型別不符合 CLS 規範
public uint GetValue() {
return 0;
}
// 警告: 參數類型“sbyte”不符合 CLS 規範
public void SetValue(sbyte a) { }
// 警告標識符“CLSTest._aFiled”不符合 CLS 規範
public string _MyProperty { get; set; }
}
可以注意到,在CLSTest類的前面為程式集加上了一個CLSCompliant特性,表明這個程式集是CLS相容的。但是,有三處並不滿足這個要求,因此編譯器給出了警告資訊。這三處是:
- 不能以大小寫來區分成員,因此欄位name和方法Name()不符合CLS。
- 方法的傳回型別和參數類型必須是CLS相容的,uint和sbyte類型並非CLS相容,因此GetValue()和SetValue()方法不符合CLS。
- 標識符的命名不能以底線“_”開頭,因此屬性_MyProperty不符合CLS。
還會注意到,編譯器給出的只是警告資訊,而非錯誤資訊,因此可以無視編譯器的警告,不過這個程式集只能由其他C#語言編寫的程式集所使用。
6.6 CLR——通用語言執行平台6.6.1 程式集概述
前面提到過:程式集包含了CIL語言代碼,而CIL語言代碼是無法直接啟動並執行,需要經過.NET運行時進行即時編譯才能轉換為電腦可以直接執行的機器指令。那麼這個過程是如何進行的呢?
接下來我們要瞭解的就是.NET架構的核心部分:CLR(Common Language Runtime),通用語言執行平台),有時也會稱做.NET運行時(.NET runtime)。在瞭解CLR之前,需要先進一步學習一下程式集,因為下一節會對程式集進行專門的講述,這裡僅簡單介紹一下程式集中對於理解CLR有協助的概念。
從直覺上來看,前面以.exe為尾碼的控制台應用程式就是一個直接的可執行檔,因為在雙擊它後,它確實會運行起來。這裡的情況和物件導向中的繼承有一點像:一台轎車首先是一部機動車、一隻貓首先是一個動物,而一個.NET程式集首先是一個Windows可執行程式。
那麼什麼樣格式的檔案才是一個Windows可執行檔?這個格式被稱做PE/COFF(Microsoft Windows Portable Executable/Common Object File Format),Windows可移植可執行/通用物件檔案格式。Windows作業系統能夠載入並運行.dll和.exe是因為它能夠理解PE/COFF檔案的格式。顯然,所有在Windows作業系統上啟動並執行程式都需要符合這個格式,當然也包括.NET程式集在內。在這一級,程式的控制權還屬於作業系統,PE/COFF頭包含了供作業系統查看和利用的資訊。此時,程式集可以表示成6-14所示。
圖6-14 程式集結構1
在前面提到過,程式集中包含的CIL語言代碼並不是電腦可以直接執行的,還需要進行即時編譯,那麼在對CIL語言代碼進行編譯前,需要先將編譯的環境運行起來,因此PE/COFF頭之後的就是CLR頭了。CLR頭最重要的作用之一就是告訴作業系統這個PE/COFF檔案是一個.NET程式集,區別於其他類型的可執行程式。
圖6-15 程式集結構2
在CLR頭之後就是大家相對熟悉一些的內容了。首先,程式集包含一個清單(manifest),這個清單相當於一個目錄,描述了程式集本身的資訊,例如程式集標識(名稱、版本、文化)、程式集包含的資源(Resources)、組成程式集的檔案等。
圖6-16 程式集結構3
清單之後就是中繼資料了。如果說清單描述了程式集自身的資訊,那麼中繼資料則描述了程式集所包含的內容。這些內容包括:程式集包含的模組(會在第7章介紹)、類型、類型的成員、類型和類型成員的可見度等。注意,中繼資料並不包含類型的實現,有點類似於C++中的.h標頭檔。在.NET中,查看中繼資料的過程就叫做反射(Reflection)。
圖6-17 程式集結構4
接下來就是已經轉換為CIL的程式碼了,也就是中繼資料中類型的實現,包括方法體、欄位等,類似於C++中的.cpp檔案。
圖6-18 程式集結構
注意,圖6-18中還多添加了一個資源檔,例如.jpg圖片。從這幅圖可以看出,程式集是自解釋型的(Self-Description),不再需要任何額外的東西,例如註冊表,就可以完整地知道程式集的一切資訊。
至此對程式集的簡單介紹就先到這裡,接下來看一下程式集是如何被執行的。
6.6.2 運行程式集
現在已經瞭解過了程式集,並且知道程式集中包含的CIL代碼並不能直接運行,還需要CLR的支援。概括來說,CLR是一個軟體層或代理,它管理了.NET程式集的執行,主要包括:管理應用程式域、載入和運行程式集、安全檢查、將CIL代碼即時編譯為機器代碼、異常處理、對象析構和記憶體回收等。相對於編譯時間(Compile time),這些過程發生在程式啟動並執行過程中,因此,將這個軟體層命名為了運行時,實際上它本身與時間是沒有太大關係的。有一些朋友在初學.NET的時候,糾結在了Runtime這個詞上,總以為和時間有什麼關係,總是不能很好地理解CLR。筆者認為重要的是理解CLR是做什麼的,而不用過於關注它的名稱。
實際上,CLR還有一種叫法,即VES(Virtual Execution System,虛擬執行系統)。從上一段的說明來看,這個命名應該更能描述CLR的作用,也不容易引起混淆,但是可能為了和CIL、CTS、CLS等術語保持一致性,最後將其命名為了CLR。在這裡,我們知道CLR不過是一個.NET程式集的運行環境而已,有點類似於Java虛擬機器。VES這個術語來自於CLI,會在6.7節進行講述。
可以用圖6-19來描述CLR的主要作用。
圖6-19 CLR的主要作用
前面已經概要地瞭解了CLR的作用,接下來開始更進一步的學習。首先遇到的問題就是:CLR以什麼樣的形式位於什麼位置?
由於CLR本身用於管理Managed 程式碼,因此它是由Unmanaged 程式碼編寫的,並不是一個包含了Managed 程式碼的程式集,也不能使用IL DASM進行查看。它位於C:\%SystemRoot%\Microsoft.NET\Framework\版本號碼下,視安裝的機器不同有兩個版本,一個是工作站版本的mscorwks.dll,一個是伺服器版本的mscorsvr.dll。wks和svr分別代表work station和server。
接下來再看一下CLR是如何運行起來的。雖然從Windows Server 2003開始,.NET架構已經預裝在作業系統中,但是它還沒有整合為作業系統的一部分。當作業系統嘗試開啟一個託管程式集(.exe)時,它首先會檢查PE頭,根據PE頭來建立合適的進程。
接下來會進一步檢查是否存在CLR頭,如果存在,就會立即載入MsCorEE.dll。這個庫檔案是.NET架構的核心組件之一,注意它也不是一個程式集。MsCorEE.dll位於C:\%SystemRoot%\System32\系統檔案夾下所有安裝了.NET架構的電腦都會有這個檔案。大家可能注意到了,這個庫安裝在System32系統檔案夾下,而沒有像其他的核心組件或類庫那樣按照版本號碼存放在C:\%SystemRoot%\Microsoft.NET\Framework\檔案夾下。這裡又存在一個“雞生蛋問題”:根據不同的程式集資訊會載入不同版本的CLR,因此載入CLR的組件就應該只有一個,不能再根據CLR的版本去決定載入CLR的組件的版本。
MsCorEE.dll是一個很細的軟體層。載入了MsCorEE.dll之後,會調用其中的_CorExeMain()函數,該函數會載入合適版本的CLR。在CLR運行之後,程式的執行權就交給了CLR。CLR會找到程式的進入點,通常是Main()方法,然後執行它。這裡又包含了以下過程:
- 載入類型。在執行Main()方法之前,首先要找到擁有Main()方法的類型並且載入這個類型。CLR中一個名為Class loader(類載入程式)的組件負責這項工作。它會從GAC、設定檔、組件中繼資料中尋找這個類型,然後將它的類型資訊載入到記憶體中的資料結構中。在Class loader找到並載入完這個類型之後,它的類型資訊會被緩衝起來,這樣就無需再次進行相同的過程。在載入這個類以後,還會為它的每個方法插入一個存根(stub)。
- 驗證。在CLR中,還存在一個驗證程式(verifier),該驗證程式的工作是在運行時確保代碼是型別安全的。它主要校正兩個方面,一個是中繼資料是正確的,一個是CIL代碼必須是型別安全的,類型的簽名必須正確。
- 即時編譯。這一步就是將託管的CIL代碼編譯為可以執行的機器代碼的過程,由CLR的即時編譯器(JIT Complier)完成。即時編譯只有在方法的第一次調用時發生。回想一下,類型載入程式會為每個方法插入一個存根。在調用方法時,CLR會檢查方法的存根,如果存根為空白,則執行JIT編譯過程,並將該方法被編譯後的本地機器代碼地址寫入到方法存根中。當第二次對同一方法進行調用時,會再次檢查這個存根,如果發現其儲存了本地機器代碼的地址,則直接跳轉到本地機器代碼進行執行,無需再次進行JIT編譯。
可以看出,採用這種架構的一個好處就是,.NET程式集可以運行在任何平台上,不管是Windows、UNIX,還是其他動作系統,只要這個平台擁有針對於該作業系統的.NET架構就可以運行.NET程式集。
6.7 CLI——公用語言基礎
CLI是一個國際標準,由ECMA和ISO進行了標準化,全稱為Common Language Infrastructure(公用語言基礎)。它只是一個概念和匯總,實際上本章的每一小節都是這個標準的一部分。CLI包括:CIL、CTS、CLS、VES、中繼資料、基礎架構。
看到這裡很多人會感覺到有點奇怪,為什麼CLI和.NET架構套件含的內容如此雷同?它們之間是什麼關係?簡單來說,CLI是一個標準,而.NET架構是這個標準的具體實現。在CLI中,並沒有CLR的概念,只有VES,而CLR就是.NET架構中VES的具體實現。既然CLI只是一個標準,而.NET架構是它在Windows平台上的具體實現,那麼是不是就只有.NET架構這一個CLI的實現?顯然不是,Mono Project就是CLI標準的另一個實現。Mono Project的目標就是將.NET架構多平台化,使其可以運行在各種平台上,包括Mac OS、Linux等。
CLI的詳細資料可以在這裡查看:http://www.ecma-international.org/publications/standards/Ecma-335.htm,感興趣的朋友可以將它的PDF標準文檔下載下來看一下。
6.8 本章小結
本章系統的學習地介紹了一下.NET架構的底層知識,幾乎包含了常見的所有術語,例如程式集、CIL、CTS、CLS、CLR等,同時也介紹了它們之間是如何相互協作共同構建起整個.NET平台的。相信經過本章的學習,大家會對.NET架構有一個更好的全域性認識。
感謝閱讀,希望這篇文章能給你帶來協助。