.NET框架

衣舞晨風發表於2015-12-01

原文地址:點選開啟連結

相關名詞解釋: 關於CLR、CIL、CTS、CLS、CLI、BCL和FCL 

三年前寫的《.NET之美》的第六章,現在書名改為了《.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提供了一個程式執行時的環境,這個執行時環境幫助我們管理記憶體、實時編譯程式、進行安全檢查、執行垃圾回收等。

接下來就針對上述內容開始為大家詳細講述。

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 Code)。相對的,不需要.NET執行時就可以執行的程式碼就叫做非託管程式碼(Unmanaged Code)。

好了,已經知道了CIL的存在,從現在開始,最好在頭腦裡建立起兩個模型或兩種視角:一種是基於C#或其他高階語言的源程式的視角,一種是基於CIL中間語言的程式集視角。C#源程式在被編譯為程式集以後,就獨立於C#,因此程式集可以由其他種類的語言所呼叫;同時,因為程式集並沒有包含本地機器的指令,所以它與具體的機器型別也分隔開了,可以被裝有.NET框架的任何機器執行。

6.3 BCL和FCL

6.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.BytebyteByte
Sytem.Int16shortShort
System.Int64intInteger

從表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——公共語言規範

既然已經理解了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,公共語言規範)。很明顯,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本身用於管理託管程式碼,因此它是由非託管程式碼編寫的,並不是一個包含了託管程式碼的程式集,也不能使用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()方法,然後執行它。這裡又包含了以下過程:

  1. 載入型別。在執行Main()方法之前,首先要找到擁有Main()方法的型別並且載入這個型別。CLR中一個名為Class loader(類載入程式)的元件負責這項工作。它會從GAC、配置檔案、程式集後設資料中尋找這個型別,然後將它的型別資訊載入到記憶體中的資料結構中。在Class loader找到並載入完這個型別之後,它的型別資訊會被快取起來,這樣就無需再次進行相同的過程。在載入這個類以後,還會為它的每個方法插入一個存根(stub)。
  2. 驗證。在CLR中,還存在一個驗證程式(verifier),該驗證程式的工作是在執行時確保程式碼是型別安全的。它主要校驗兩個方面,一個是後設資料是正確的,一個是CIL程式碼必須是型別安全的,型別的簽名必須正確。
  3. 即時編譯。這一步就是將託管的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框架有一個更好的全域性性認識。

感謝閱讀,希望這篇文章能給你帶來幫助。











相關文章