C#基礎語言知識--編譯和執行過程(二)

3.加載公共語言運行時
  生成的每個程序集既可以是可執行應用程序,也可以是DLL。當然,最終是由CLR管理這些程序集中的代碼的執行。這意味着目標機器必須安裝好.Net Framework。

  要知道是否已安裝.Net Framwork,只需檢查%SystemRoot%\System32目錄中的MsCorEE.dll文件。存在該文件,表明.Net Framework已安裝。

  如果程序集文件值包含類型安全的代碼,代碼在32位和64位Windows上都能正常工作。在這兩種Windows上運行,源代碼無需任何改動。事實上,編譯器最終生成的EXE/DLL文件在Windows的x86和x64版本上都能正常工作。在極少數情況下,開發人員希望代碼只在一個特定版本的Windows上運行。例如,要使用不安全的代碼,或者要和麪向一種特定CPU架構的非託管代碼進行互操作,就可能需要這樣做。
  
  C#編譯器提供了一個/platform命令行開關選項。這個開關允許指定最終生成的程序集只能在運行32位Windows版本的x86機器上使用,只能在運行64位Windows版本的x64機器上使用。不指定具體平臺的話,默認選項就是anycpu,表明最終生成的程序集能在任何版本的Windows上運行。
這裏寫圖片描述

  下表總結了兩方面的信息。其一,爲C#編譯器指定不同/platform命令行開關將得到哪種託管代碼。其二,應用程序在不同版本的Windows上如何運行。
這裏寫圖片描述
Windows檢查EXE文件頭,決定是創建32位還是64位進程後,會在進程地址空間加載MSCorEE.dll的x86,x64或ARM版本。如果是Windows的x86或ARM版本,MSCorEE.dll的x86版本在%SystemRoot%\System32目錄中。如果是Windows的x64版本,MSCorEE.dll的x86版本在%SystemRoot%\SysWow64目錄中,64位版本則在%SystemRoot%\System32目錄中。然後,進程的主線程調用MSCorEE.dll中定義的一個方法。這個方法初始化CLR,加載EXE程序集,再調用其入口方法(Main)。隨即,託管應用程序啓動並運行。

4.執行程序集的代碼
  爲了執行一個方法,首先必須把它的IL轉換成本地CPU指令。這是CLR的JIT(just-in-time或者“即時”)編譯器的職責。

  下圖展示了一個方法首次調用時發生的事情。
  這裏寫圖片描述

  就在Main方法執行之前,CLR會檢測出Main的代碼引用的所有類型。這導致CLR分配一個內部數據結構,它用於管理對所引用的類型的訪問。在圖1-4中,Main方法引用了一個Console類型,這導致CLR分配一個內部結構。在這個內部數據結構中,Console類型定義的每個方法都有一個對應的記錄項。每個記錄項都容納了一個地址,根據此地址即可找到方法的實現。對這個結構進行初始化時,CLR將每個記錄項都設置成(指向)包含在CLR內部的一個文檔化的函數。我將這個函數稱爲JITCompiler。

  Main方法首次調用WriteLine時,JITCompiler函數會被調用。JITCompiler函數負責將一個方法的IL代碼編譯成本地CPU指令。由於IL是“即時”(just in time)編譯的,所以通常將CLR的這個組件成爲JITter或者JIT編譯器。
  
  JITCompiler函數被調用時,它知道要調用的是哪個方法,以及具體是什麼類型定義了該方法。然後,JITCompiler會在定義(該類型的)程序集的元數據中查找被調用的方法的IL。接着,JITCompiler驗證IL代碼,並將IL代碼編譯成本地CPU指令。本地CPU指令被保存到一個動態分配的內存塊中。然後JITCompiler返回CLR爲類型創建的內部數據結構,找到與被調用的方法對應的那一條記錄,修改最初對JITCompiler的引用,讓它現在指向內存塊(其中包含了剛纔編譯好的本地CPU指令)的地址。最後,JITCompiler函數跳轉到內存塊中的代碼。這些代碼正是WriteLine方法(獲取單個String參數的那個版本)的具體實現。這些代碼執行完畢並返回時,會返回至Main中的代碼,並跟往常一樣繼續執行。

  現在,Main要第二次調用WriteLine。這一次,由於已對WriteLine的代碼進行了驗證和編譯,所以會直接執行內存塊中的代碼,完全跳過JITCompiler函數。WriteLine方法執行完畢之後,會再次返回Main。
  
  下圖展示了第二次調用WriteLine時發生的事情。
  這裏寫圖片描述

  一個方法只有在首次調用時纔會造成一些性能損失。以後對該方法的所有調用都以本地代碼的形式全速運行,無需重新驗證IL並把它編譯成本地代碼。

  JIT編譯器將本地CPU指令存儲到動態內存中。一旦應用程序終止,編譯好的代碼也會被丟棄。所以,如果將來再次運行應用程序,或者同時啓動應用程序的兩個實例(使用兩個不同的操作系統進程),JIT編譯器必須再次將IL編譯成本地指令。

  對於大多數應用程序,因JIT編譯造成的性能損失並不顯著。大多數應用程序都會反覆調用相同的方法。在應用程序運行期間,這些方法只會對性能造成一次性的影響。另外,在方法內部花費的時間很有可能被花在調用方法上的時間多得多。

5.通用類型系統
CLR是完全圍繞類型展開的,這一點到現在爲止應該很明顯了。類型爲應用程序和其他類型公開了功能。通過類型,用一種編程語言寫的代碼能與用另一種語言寫的代碼溝通。由於類型是CLR的根本,所以Microsoft制定了一個正式的規範,叫做“通用類型系統”(Common Type System,CTS),它描述了類型的定義和行爲。
  
CTS規範規定,一個類型可以包含零個或者多個成員。本書第II部分“設計類型”將更詳細地討論這些成員。目前只是簡單地介紹一下它們。

  • 字段(Field) 一個數據變量,是對象狀態的一部分。字段根據名稱和類型來區分。
  • 方法(Method) 一個函數,能針對對象執行一個操作,通常會改變對象的狀態。方法有一個名稱、一個簽名以及一個或多個修飾符。簽名指定參數的數量(及其順序);參數的類型;方法是否有返回值;如果有返回值,還要指定返回值的類型。
  • 屬性(Property) 對於調用者,該成員看起來像是一個字段。但對於類型的實現者,它看起來像是一個方法(或者兩個方法,稱爲getter和setter,或者稱爲取值方法和賦值方法)。屬性允許實現者在訪問值之前對輸入參數和對象狀態進行校驗,以及/或者只有在必要的時候才計算一個值。屬性還允許類型的用戶採用簡化的語法。最後,可利用屬性創建只讀或只寫的“字段”。
  • 事件(Event) 事件在對象以及其他相關對象之間實現了一個通知機制。例如,利用按鈕提供的一個事件,可以在按鈕被單擊之後通知其他對象。

CTS還指定了類型可視化規則以及類型成員的訪問規則。例如,如果將類型標記爲public(在C#中使用public修飾符),任何程序集都能看見並訪問該類型。但是,如果將類型標記爲assembly(在C#中使用internal修飾符),只有同一個程序集中的代碼才能看見並訪問該類型。所以,利用CTS制定的規則,程序集爲一個類型建立了可視邊界,CLR則強制(貫徹)了這些規則。

調用者雖然能“看見”一個類型,但並不是說就能隨心所欲地訪問它。利用一下選項,可進一步限制調用者對類型中的成員的訪問。

  • private 成員只能由同一個類(class)類型中的其他成員訪問。
  • family 成員可由派生類型訪問,不管那些類型是否在同一個程序集中。注意,許多語言(比如C++和C#)都用protected修飾符來表示family。
  • family and assembly 成員可由派生類型訪問,但這些派生類型必須是在同一個程序集中定義的。許多語言(比如C#和Visual Basic)都沒有提供這種訪問控制。當然,IL彙編語言不在此列。
  • assbmly 成員可由同一個程序集中的任何代碼訪問。許多語言都用internal修飾符來標識assembly。
  • family or assembly 成員可由任何程序集中的派生類型訪問。成員也可由同一個程序集中的任何類型訪問。在C#中,是用protected internal修飾符來標識family or assembly。
  • public 成員可由任何程序集中的任何代碼訪問。

除此之外,CTS還爲類型繼承、虛方法、對象生存期等定義了相應的規則。這些規則在設計之初,並順應了可以用現代編程語言來表示的語義。事實上,根本不需要專門去學習CTS規則本身,因爲你選擇的語言會採用你熟悉的方式公開它自己的語言語法與類型規則。通過編譯來生成程序集時,它會將語言特有的語法映射到IL–也就是CLR的“語言”。
  
下面是另一條CTS規則:所有類型最終必須從預定義的System.Object類型繼承。可以看出,Object是System命名空間中定義的一個類型的名稱。Object是其他所有類型的根,因爲保證了每個類型實例都有一組最基本的行爲。具體地說,System.Object類型允許做下面這些事情:

  • 比較兩個實例的相等性(Equals)
  • 獲取實例的哈希碼(GetHashCode)
  • 查詢一個實例的真正類型(GetType)
  • 執行實例的(淺)拷貝(MemberwiseClone)
  • 獲取實例對象的當前狀態的一個字符串表示(ToString)

6.公共語言規範
  要創建很容易從其他編程語言中訪問的類型,只能從自己的編程語言中挑選其他所有語言都確定支持的那些功能。爲了在這個方面提供幫助,Microsoft定義了一個“公共語言規範”(Common Language Specification,CLS),它詳細定義了一個最小功能集。任何編譯器生成的類型要想兼容於由其他“符合CLS、面向CLR的語言”所生產的組件,就必須支持這個最小功能集。

  CLR/CTS支持的功能比CLS定義的子集多得多。如果不關心語言之間的互操作性,可以開發一套功能非常豐富的類型,它們僅受你選用的那種語言的功能集的限制。具體地說,在開發類型和方法的時候,如果希望它們對外“可見”,能夠從符合CLS的任何一種編程語言中訪問,就必須遵守由CLS定義的規則。注意,假如代碼只是從定義(這些代碼的)程序集的內部訪問,CLS規則就不適用了。
  下圖形象地演示了這一段想要表達的意思。
  這裏寫圖片描述
  
  上圖所示,CLR/CTS提供了一個功能集。有的語言公開了CLR/CTS的一個較大的子集。例如,假定開發人員使用IL彙編語言寫程序,就可以使用CLR/CTS提供的全部功能。但是,其他大多數語言(比如C#、Visual Basic和Fortran)只向開發人員公開了CLR/CTS的一個功能子集。CLS定義了所有語言都必須支持的一個最小功能集。

以下代碼使用C#定義一個符合CLS的類型。然而,類型中含有幾個不符合CLS的構造,造成C#編譯器報錯:

<span style="font-size: 14px;">using System;
//  告訴編譯器檢查CLS相容性
[assembly: CLSCompliant(true)]

namespace SomeLibrary
{
    //  因爲是public類,所以會顯示警告

    public sealed class SomeLibraryType
    {
        //  警告:SomeLibrary.SomeLibraryType.Abc()的返回類型不符合CLS
        public UInt32 Abc() { return 0; }

        //  警告:僅大小寫不同的標識符SomeLibrary.SomeLibraryType.abc()不符合CLS
        public void abc() { }

        //  不會顯示警告:該方法是私有的
        private UInt32 ABC() { return 0; }

    }
}
</span

  上述代碼將[assembly:CLSCompliant(true)]這個attribute1應用於程序集。這個attribute告訴編譯器檢查public類型,判斷是否存在任何不合適的構造,阻止了從其他編程語言中訪問該類型。上述代碼編譯時,C#編譯器會報告兩條警告消息。第一個警告是因爲Abc方法返回了一個無符號整數;有一些語言是不能操作無符號整數值的。第二個警告是因爲該類型公開了兩個public方法,這兩個方法(Abc和abc)只是大小寫和返回類型有別。Visual Basic和其他一些語言無法區別這兩個方法。

  有趣的是,刪除sealed class SomeLibraryType之前的public字樣,然後重新編譯,兩個警告都會消失。因爲這樣一來,SomeLibraryType類型將默認爲internal(而不是public),將不再向程序集的外部公開。要獲得完整的CLS規則列表,請參見.NET Framework SDK文檔的“跨語言互操作性”一節(http://msdn.microsoft.com/zh-cn/library/730f1wy3.aspx)。

  現在,讓我們提煉一下CLS的規則。在CLR中,一個類型的每個成員要麼是一個字段(數據),要麼是一個方法(行爲)。這意味着每一種編程語言都必須能訪問字段和調用方法。這些字段和方法通過特殊或者通用的方式來使用。爲了編程進行編程,語言通常提供了額外的抽象,對這些常見的編程模式進行簡化。例如,語言可能公開枚舉、數組、屬性、索引器、委託、事件、構造器、析構器、操作符重載、轉換操作符等概念。編譯器在源代碼中遇到上述任何一種構造,必須將其轉換成字段和方法,使CLR和其他編程語言能夠訪問這些構造。

7.編譯和執行過程總結
  下圖簡要說明了上述特性在編譯和執行過程中如何發揮作用。
  這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章