微軟.NET Framework介紹了很多概念、技術和術語。在這一章我的目标是給你一個概述,.NET Framework是怎麼設計的,介紹一些架構包含的技術,和很多定義的術語,當你開始使用.NET Framework的時候将會看到這些。我也将通過帶你建立你自己的源碼應用程式或者一個可再使用元件(檔案集)集合包含(類,枚舉,等等)向你解釋一個應用程式是将怎麼執行。
Compiling Source Code into Managed Modules(編譯源碼到托管子產品)
那麼你已經決定使用.NET Framework作為你的開發平台了。你的第一步是決定你想要開發的是哪一種類型的應用程式或者元件。讓我們來假定你已經完成了這個小細節;每一件事都已經設計好了,規格說明書都已近寫好了,并且你已經準備開發了。
現在你必須決定要使用哪一種開發語言。一般而言,這個任務有點難度,因為不同的語言擁有不同的能力。比如,非托管的C/C++,你能控制底層的系統。你能通過你想要的方式精确的管理記憶體,當你想建立線程的時候很容易,等等。微軟Visual Basic 6.0,在另一方面,允許你快速的建立UI應用程式和可以讓你容易的控制COM元件和資料庫。
公共語言運作庫(CLR)正如它的名字一樣:runtime可用于不同的和各種各樣的程式設計語言。CLR的核心特征(比如記憶體管理,程式集加載,安全,異常處理,和線程同步)可适用于任何和所有程式設計語言隻要編譯目标期間是CLR。比如,runtime使用異常報告錯誤,是以所有編譯目标是runtime的程式設計語言獲得錯誤報告都是通過異常得到的。另一個例子是runtime允許你建立線程,是以所有編譯目标是runtime的程式設計語言都可以建立線程。
事實上,在runtime庫中,CLR不知道開發者使用哪一種開發語言寫的源碼。這意味着你選擇的開發語言應該是最容易表達你的意圖的。你可以用任何你想用的開發語言隻要你使用的編譯器能把你的代碼程式設計成CLR。
是以,假如我說的真的,使用某個開發語言而不使用另一個開發語言有什麼好處?好吧,我認為編譯器作為文法檢查者和"代碼糾錯"分析者。它們檢查你的源代碼,確定你寫的源碼有一些道理,然後輸出描述你意圖的代碼。不同的程式設計語言允許你在開發時使用不同的文法。不要低估選擇開發語言的價值。比如,對于數學或者财政應用程式,使用APL文法表達你的開發意圖可以節省很多天開發時間相較于使用Perl文法表達相同的開發意圖。
微軟已經建立了幾門語言編譯器編譯成runtime:C++、CLI,C#(發音"C sharp"),Visual Basic,F#(發音"F sharp"),Iron Python,Iron Ruby,和IL彙編。除微軟外,另外幾家公司、大學都建立了編譯器并且産生的代碼目标是CLR。我知道的編譯器可編譯的語言有Ada,APL,Caml,COBOL,Eiffel,Forth,Fortran,Haskell,Lexico,LISP,LOGO,Lua,Mercury,ML,Mondrian,Oberon,Pascal,Perl,PHP,Prolog,RPG,Scheme,SmallTalk,和 Tcl/Tk。
下圖指出了程式編譯源碼檔案。如圖所示,你可以使用任何支援CLR的程式設計語言建立源碼檔案集。然後你可以使用相應的編譯器檢查文法和分析源碼。不管你使用哪一個編譯器,結果都是托管元件。一個托管子產品是一個标準的32位Windows PE32檔案或者标準64位Windows PE32+檔案隻能在CLR執行。順便說一句,在Windows中托管程式集會得到DEP和ASLR的好處,這兩個特征提高了你整個系統的安全性。
圖表描述了托管子產品的元件。
托管元件組成部分
元件名稱 | 描述 |
PE32或者PE32+ header | 标準Windows PE檔案頭,它和COFF(Common Object File Format)的頭檔案很像。假如頭檔案使用PE32轉換,那麼所轉換檔案可在32位或者64位的Windows系統上運作。假如頭檔案使用PE32+轉換,那麼檔案隻能在64位版本的Windows系統上運作。頭檔案也規定了檔案的格式:GUI,CUI,或者DLL,并且包含一個檔案何時建立的時間戳。對于隻包含IL代碼的子產品,大部分在PE32(+)頭檔案中的資訊都會被忽略。對于包含本地CPU代碼的子產品,這個頭檔案包含了本地CPU代碼資訊。 |
CLR header | 包含标記目前子產品為托管子產品的資訊(通過CLR和工具解釋得來的資訊)。頭檔案包含所需的CLR版本,一些标記,托管子產品方法(Main method)入口點的MethodDef中繼資料令牌。子產品中繼資料的位置和大小,資源,強命名,一些标記,和其他少量有趣的東西。 |
中繼資料 | 每個托管子產品都包含中繼資料表單。所有的表單中有兩個主要的分類:一種是描述你源碼中定義的類型和成員,另一種是描述源碼中的被引用的類型和成員。 |
IL Coder | 編譯器編譯産生的代碼。在運作時,CLR把IL編譯成本地CPU指令。 |
本地代碼編譯器按照指定的CPU架構生成代碼,比如x86,x64,或者ARM。所有順從CLR編譯器的都會生成IL。(我在後面的章節會深入更多的IL代碼細節。)IL代碼有時會被歸為托管代碼,因為CLR管理它的執行。
除了生成IL,每一個以CLR為編譯目标的編譯器都要求在每個托管子產品中生成所有中繼資料。簡而言之,中繼資料是資料表的一個集合,描述了在子產品中定義了什麼,比如類型和成員。此外,中繼資料也有表指出托管子產品的引用,比如導入的類型和導入的成員。中繼資料是老技術的一個超集,比如COM's Type Libraries和Interface Definition Language(IDL) 檔案。重點需要注意的是CLR中繼資料更完整。并且,不同于Type Libraries和IDL,中繼資料一直是和包含IL代碼的檔案是關聯的。事實上,中繼資料一直作為代碼嵌入到相同名字的EXE/DLL中,使具有相同名字的EXE/DLL不能分離。因為編譯器生成中繼資料和代碼的同時把它們綁定到托管子產品,中繼資料和IL代碼不能分開描述。
下面是中繼資料的一些用處:
*編譯時中繼資料移除了本地的C/C++頭檔案和庫檔案,因為所有類型/成員引用資訊已經包含在IL中,IL實作了類型/成員。編譯器可以直接從托管子產品中讀取中繼資料。
*微軟Visual Studio使用中繼資料幫助你寫代碼。它的智能提示特性通過轉換中繼資料告訴你方法需要的屬性,事件,和提供的字段類型,在一個方法中該方法需要什麼參數。
*CLR的代碼驗證程式使用中繼資料確定你的代碼執行時類型安全。
*中繼資料允許一個對象的字段系列化為記憶體塊,發送給另一個機器,然後反系列化,在遠端機器上重建對象的狀态。
*中繼資料允許垃圾回收器跟蹤對象的生命周期。對于任何對象來說,垃圾回收器能決定對象是何種類型,通過中繼資料,知道哪一個對象所包含的字段被另一個對象引用。
在第二章,"生成,打包,部署,管理程式和類型",我将講更多的中繼資料細節。
微軟的C#,Visual Basic,F#,和IL Assembler總是生成包含托管代碼(IL)和托管資料(回收資料類型)的子產品。為了執行包含托管代碼或者托管資料的子產品,最終使用者必須在他們的機器安裝了CLR(目前作為.NET Framework的一部分),同樣的,他們也需要安裝Microsoft Foundation Class(MFC)庫或者Visual Basic DLLs才能運作MFC或者Visual Basic 6.0程式。
預設的,微軟的C++編譯器生成包含非托管(本地)代碼和可以操作非托管資料(本地記憶體)EXE/DLL的子產品在運作時。這些子產品不需要CLR執行。無論如何,通過指定CLR指令行轉換,C++編譯器生成的子產品将包含托管代碼,這樣一來,要執行這些代碼就需要安裝CLR了。微軟所有的編譯器都提到,C++是唯一編譯器可允許程式員寫托管和非托管代碼的程式設計語言并且放到一個子產品中。C++也是微軟編譯器唯一允許開發者在源碼中定義托管和非托管資料類型的語言。和其他編譯器相比微軟的C++編譯器的靈活性是無以倫比的,因為它允許開發者使用已存在的本地托管C/C++代碼并且開始內建開發者看到适合的托管類型。
Combining Managed Modules into Assemblies(組合托管子產品為程式集)
CLR實際上不是依靠子產品工作,而是依賴程式集。程式集是一個抽象概念剛開始很難領會。首先,一個程式集是一個邏輯組對應一個或多個子產品或源檔案集。第二,一個程式集是可重用的、安全的、版本化的最小單元。根據你使用的編譯器或工具你可以選擇生成一個檔案或一個多檔案程式集。在CLR的世界裡,一個程式集就是我們叫的元件。
在第二章中,我将十分詳細的重溫程式集,是以我不打算在這花太多時間在程式集上。現在我所要做的是讓你知道一個額外的概念,一種把一組檔案當作一個實體的思路。
下圖應該可以幫助解釋程式集是什麼。在這張圖中,一些托管子產品是被一個工具加工過的源檔案(或資料)檔案。這個工具每産生一個單獨的PE32(+)檔案就代表着一個經過邏輯分組的檔案集。這個PE32(+)檔案包含了一塊資料被稱作載貨單。載貨單是中繼資料表其中一個簡單的集合。這些表描述了檔案如何組成程式集合,公開導出類型實作的檔案在集合中,并且資源或者資料檔案與程式集合都有關聯。
上圖指出了如何把托管子產品組裝到程式集。
預設的,編譯器實際做的工作是把分散的托管子產品轉換成一個程式集合;C#編譯器發出一個托管子產品包含一個載貨單。載貨單現實一個程式集僅僅隻由一個檔案組成。是以,對于隻有一個托管子產品并且沒有資源(或資料)檔案的項目來說,程式集就是托管子產品,當你生成程式的時候不需要任何附加的步驟。如果你要把一個檔案集合分組生成程式集,那麼你不得不知道更多的工具(比如程式集連結者,AL.exe)和它們的指令行選項。我将在第二章中解釋這些工具和選項。
一個程式集允許你以可重用、安全的、無版本沖突的理念在邏輯和實體上解耦。你怎麼劃分你的代碼和資源到不同的文檔完全取決于你。比如說,你可以把很少用的類型或者資源放到分離的文檔中,它是程式集的一部分。分離的文檔在運作需要的時候會從網絡上下載下傳。假如文檔從來沒有被用到,該文檔永遠不會被下載下傳,節省了磁盤空間和減少安裝時間。程式集允許你打碎部署檔案,但是依然把所有的檔案看做一個集合。
一個程式集的子產品也包括被引用程式集的資訊(包括它們的版本号)。這個資訊是一個程式集的自描述。換句話說,CLR可以确定程式集的直接依賴以便代碼在程式集中能正确執行。在系統資料庫或者活動目錄域服務(Active Directory Domain Services,AD DS)中是不需要附加資訊的。由于不需要附加資訊,部署程式集比部署非托管元件容易多了。
Loading the Common Language Runtime(加載公共語言運作庫)
每一個你生成的程式集都是一個可執行應用程式或者一個DLL(這個DLL包含一個執行應用程式的類型集合)。當然,CLR負責管理包含在這些程式集中的代碼。這意味着必須在主機上安裝.NET Framework。微軟已經建立了一個再分布的包你可以免費下載下傳安裝.NET Framework到你的客戶的機器上。一些版本的Windows已經自帶了.NET Framework。
你可以通過查找%SystemRoot%\System32目錄下的MSCorEE.dll檔案确定.NET Framework是否已經正确安裝。檔案存在則說明.NET Framework已經安裝了。無論如何,在一台機器上可以同時安裝幾個版本的.NET Framework。假如你要确定哪一個版本的.NET Framework确實被安裝了,檢查以下的子目錄。
%SystemRoot%\Microsoft.NET\Framework
%SystemRoot%\Microsoft.NET\Framework64
.NET Framework SDK包含一個叫做CLRVer.exe的指令行工具,這個工具可以顯示出所有的安裝在機器上的.NET Framework。這個工具還可以顯示出哪一個本來的CLR正被程式使用,輸入-all開關或者輸入你感興趣的程序ID以檢視。
在我們開始考慮CLR怎樣加載之前,我們需要花費一點時間讨論32位和64位版本的Windows。假如你的程式集檔案隻包含類型安全的托管代碼,那麼你寫的代碼應該可以在32位和64位版本的Windows上運作。不需要改變你的源代碼以适配兩個版本的Windows。事實上,編譯器産生的EXE/DLL檔案應該能正确運作不論是x86還是x64版本的Windows。此外,微軟商店的應用程式或者類庫也可以在Windows RT機器(使用ARM CPU)上運作。換句話說,一個檔案可以在任何機器上運作隻要在該機器上安裝了相應的.NET Framework。
在極限少數場景,開發者要寫指定版本的Windows的代碼。當使用非安全代碼或者當與非托管代碼互動操作的時候開發者可能需要寫指定CPU架構的代碼。為了幫助這些開發者,C#編譯器提供了一個平台指令行開關。這個開關允許你指定是否結果集可以運作在x86機器隻在32位的Windows版本運作,64位機器隻運作64位Windows,或者ARM機器隻運作32位Windows RT。假如你不指定平台,預設的是anycpu,這意味着結果程式集可以在任何版本的Windows上運作。VS使用者可以在項目上設定目标平台通過顯示項目的屬性頁,點選生成标簽,然後選擇目标平台。
在下圖中,你會發現首選32位選擇框。這個選擇框隻有在目标平台設定為Any CPU時才會啟用,并且項目是一個可執行的類型。如果你勾中首選32位,那麼Visual Studio的C#編譯器指定平台:anycpu32bitpreferred編譯器開關。這個選項指出可執行檔案應該在32位的機器上執行,即使正在64的機器上運作。假如你的應用程式不要求額外的記憶體以運作64位程式,那麼勾中首選32位是一個有代表性的好方法因為Visual Studio不支援對x64程式編輯并繼續。此外,32位程式能和32位DLL和COM元件互動操作假如你的程式需要。
依賴于平台開關,C#編譯器将會發行一個包含PE32或PE32+頭檔案的程式集。編譯器也會在頭檔案中發行一個需要的CPU架構(或者不可知的架構)。微軟搭載了兩個SDK指令行工具,DumpBin.exe和CorFlags.exe,通過編譯器你可以用來檢查釋出在托管子產品中的頭資訊。
當運作一個可執行檔案時,Windows檢查這個EXE檔案的頭檔案确定應用程式是需要32位的還是64位的位址空間。一個帶有PE32頭檔案的檔案可以在32位或者64位的位址空間運作,但是一個帶有PE32+頭檔案的檔案需要64位位址空間。Windows也檢查嵌入在頭檔案中的CPU架構資訊以確定電腦上的CPU類型比對。最後,64位Windows提供了一種技術允許32位Windows應用程式運作。這個技術叫做WoW64(for Windows on Windows 64)。
下圖顯示了兩件事情。第一,它顯示了當你給C#編譯器指定何種平台指令行開關你将得到何種托管子產品。第二,它指出了各種應用程式能在哪些版本的Windows上運作。
當生成子產品和在運作時在各個平台的效果
在Windows已經檢查了EXE檔案的頭檔案後決定是否建立32位或64位程式,然後Windows加載x86,x64或者ARM版本的MSCorEE.dll到程式的位址空間。在一個x86或ARM版本的Windows上,32位版本的MSCorEE.dll将在%SystemRoot%\System32檔案夾下找到。x64版本的Windows上,x86版本的MSCorEE.dll将在%SystemRoot%\SysWow64檔案夾下找到。反之64位版本的可以在%SystemRoot%\System32檔案夾下找到(由于向後相容的原因)。然後程式的主線程調用一個定義在MSCorEE.dll中方法。這個方法會初始化CLR,加載EXE程式集,然後調用入口标點方法(Main)。在這個标點,托管程式啟動并且運作。(你的代碼可以查詢環境的Is64BitOperatingSystem屬性确定程式是否是64位的Windows版本。你的代碼也可以查詢環境的Is64BitProcess屬性确定是否使用64位位址空間。)
注意 使用1.0或1.1版本的微軟C#編譯器生成的程式集将包含一個PE32的頭檔案并且是CPU架構無關的。無論如何,加載時,CLR認為這些程式集都是隻生成x86的。對于執行檔案,這提高了應用程式确實可以在64位作業系統上工作的可能性,因為可執行檔案将會加載WoW64,給程序與32位x86相似的環境。 |
假如一個未托管應用程式調用Win32 LoadLibrary功能加載托管程式集,Windows知道要加載和初始化CLR(假如未加載)去處理包含程式集的代碼。當然,在這樣的情景下,程式已經啟動并運作了,并且可能影響到程式集的可用性。比如,打開解決方案平台x86開關編譯的托管程式集就絕不會加載到64位程式,反之,在一個運作64位版本Windows的電腦上打開x86開關編譯的可執行檔案将會在WoW64中加載。
Executing Your Assembly’s Code(執行你的程式集代碼)
就像之前提到過的一樣,托管程式集包含中繼資料和IL。IL是一個不依賴CPU的機器語言,微軟在經過幾次外部商業和學術言語編譯器編寫者的讨論後建立了它。IL是一個比較進階的言語相比于大多數CPU機器語言來說。IL可以通路和操作對象類型并發出指令建立和初始化對象,調用對象中的虛拟方法,直接操作數組元素。它甚至可以發出指令處理異常。你可以認為IL是一個面向對象的機器語言。
通常,開發者會用一個進階語言,比如C#,Visual Basic,或者F#。編譯器針對這些進階語言會生成IL。然而,像其他的機器語言一樣,IL也可以使用彙編語言編寫,并且微軟提供了一個IL彙編,ILAsm.exe。微軟也提供了一個IL反彙編,ILDasm.exe。
記住,任何進階語言最多隻會暴露CLR的一個工具子集。但是,IL彙編語言允許開發者通路CLR的所有工具。是以,當你要為你的程式設計語言選擇一個對你有利的CLR隐藏工具時,你可以選擇用IL彙編寫你的那部分代碼或者另一種包含你需要的特性的程式設計語言。
唯一了解CLR提供了什麼工具的方法是閱讀CLR自己提供的文檔。在這本書中,我盡量把重點放在CLR的特征上,通過C#語言怎麼暴露或不暴露這些特征。我猜大多數其他的書或者文章将會通過一門語言的視角呈現CLR,并且大多數開發者相信CLR暴露的工具隻限于開發者選擇的語言。隻要你的語言允許你完成你想要完成的,這個模糊的觀點就不是壞事。
重點 我認為在能在語言之間通過豐富的內建簡單轉換開發語言的能力是CLR非常棒的特征。不幸的是,我也相信開發者常常會忽略這個特征。開發語言比如C#和Visual Basic是很好的執行I/O操作的語言。對于執行進階工程學或者财政計算APL是一門很好的語言。通過CLR,你可以在你的應用程式中使用C#寫I\O部分,使用APL寫工程學計算部分。CLR在這些程式設計語言之間提供了一層內建,這是空前的,在有的項目中使混合語言程式設計值得列入考慮。 |
要執行一個方法,它的IL必須先轉換成本地CPU指令。這是CLR實時編譯器(just-in-time compiler)的工作。
下圖示例是方法第一次被調用發生了什麼。
僅僅在Main方法調用前,CLR檢測所有被Main的代碼引用的到的類型。這是因為CLR要配置設定一個内部資料結構用于管理引用類型的通路。在下圖中,Main方法适用于一個單類型,Console,成為單類型的原因是CLR配置設定單個内部結構。在Console類型中定義後,針對每個方法這個内部結構包含一個入口。每個入口持有一個方法的實作位址。當初始化這個結構時,CLR把每個入口設定為内部的,CLR自己包含非文檔化的功能。我把這個功能叫做JITCompliler(JIT編譯器)。
當Main第一次調用WriteLine時,JITCompiler功能被調用了。JITCompiler功能負責編譯方法的IL代碼生成本地CPU指令。因為IL是被實時編譯的,CLR這個元件常常被歸為一個JITter或者一個JIT編譯器。
注意 假如應用程式在x86版本的Windows上運作或者使用WoW64技術,JIT編譯器生成x86指令。假如你的應用程式是64位版本并且在x64版本的Windows上運作,JIT編譯器生成x64指令。假如應用程式在ARM版本的Windows上運作,那麼JIT編譯器生成ARM指令。 |
第一次調用一個方法
當被調用時,JITCompiler功能知道什麼方法被調用了和什麼類型定義這個方法。JITCompiler功能然後搜尋定義程式集的中繼資料以調用方法的IL。JITCompiler接下來驗證和編譯IL代碼生成本地CPU指令。本地CPU指令被儲存在動态配置設定記憶體塊中。然後,JITCompiler傳回到CLR建立的類型的内部資料結構并調用方法入口,然後使用剛編譯好的包含本地CPU指令的記憶體塊位址替換第一次調用它的引用位址。最後,JITCompiler功能跳到記憶體塊代碼。這份代碼是WriteLine方法的實作(一個包含String參數的版本)。當這份代碼傳回,它傳回到在Main中的代碼,代碼繼續正常執行。
Main現在第二次調用WriteLine。這次,WriteLine代碼已經被驗證和編譯過了。是以調用直接去到記憶體塊,完全跳過JITCompiler功能。在WriteLine方法執行後,它傳回到Main。下圖顯示了當WriteLine第二次被調用時程式看上去是什麼樣。
隻有第一次調用方法時才會有性能損失。随後所有的方法調用執行本地代碼都會全速執行,因為對于本地代碼不會再執行驗證。
JIT編譯器在動态記憶體中存儲本地CPU指令。這意味着當應用程式結束時編譯過的代碼會被丢棄。假如你要再運作一次應用程式或者你同時運作兩個應用程式執行個體(在兩個不同的作業系統程序),JIT編譯器将再把IL編譯成本地代碼一次。依賴于應用程式,這将明顯的增加記憶體消耗相比較于一個本地應用程式,這個本地應用程式的隻讀代碼能被所有運作執行個體共享。
對于大多數應用程式來說,JIT編譯影響的性能并不明顯。大多數應用程式傾向于一遍又一遍的調用相同的幾個方法。這些方法隻會在應用程式執行時影響一次性能。它也可能花費更多時間在方法内部相比于隻調用方法。
你應該知道CLR的JIT編譯器優化本地代碼就像後端的一個非托管C++編譯器一樣。然而,它可能會花費更多的時間以優化代碼,但是與未優化的代碼相比優化過的代碼會有更佳的性能。
有兩個C#編譯器開關會影響代碼優化:/optimize和/debug。下面的圖表顯示了這些開關對代碼品質的影響,通過C#編譯器生成的IL代碼和通過JIT編譯器生成的本地代碼。
打開/optimize-,C#編譯器生成的非優化代碼包含很多非操作(NOP)指令和跳到下一行代碼的分支。這些指令釋出是為了在調試時啟用Visual Studio的edit-and-continue特征,額外的指令允許在流程控制指令比如for,while,do,if,else,try,catch,和finally聲明塊設定斷點以便代碼更容易調試。當生成優化的IL代碼時,C#編譯器将會移除無關的NOP和分支指令,使代碼難以單步調試因為流程控制被優化了。同樣的,一些功能評價在内部調試時可能也不工作了。無論如何,IL代碼更少了,結果是EXE/DLL檔案也更小了,對于喜歡檢視編譯器生成的IL的人來說,IL更好閱讀了。
此外,在你指定/debug(+/full/pdbonly)開關後編譯器會生成一個Program Database(PDB)檔案。PDB檔案幫助調試器找到本地變量并把IL指令映射到源代碼。/debug:full開關告訴JIT編譯器你要調試程式集,JIT編譯器将會跟蹤來自每個IL指令的本地代碼。這允許你使用Visual Studio的實時調試器(just-in-time debugger)特性,連接配接到一個調試器上調試已經在運作的程序。沒有/debug:打開full開關,JIT編譯器的預設設定不會跟蹤IL到本地代碼資訊,這使得JIT編譯器運作的稍微快一點和使用稍少的記憶體。假如你用Visual Studio調試器開始一個程序,它會強制JIT編譯器跟蹤IL到本地代碼資訊(不管/debug開關)除非你關掉Visual Studio中在子產品加載時取消JIT優化(僅限托管)選項。
當你在Visual Studio中建立一個新的C#項目,Debug配置項有/optimize-和/debug:full開關,在Release配置項中指定了/optimize+和/debug:pdbonly開關。
對于那些來自帶有非托管C或者C++背景的開發者來說,你可能考慮的是這一些之外的性能分支。畢竟,非托管代碼編譯針對的是一個指定的CPU平台,并且,當被調用後,代碼可以簡單的執行。在托管的環境中,需要兩個步驟完成代碼編譯。首先,編譯器忽略源碼,做盡可能多的工作生成IL。但是要執行代碼,在運作時IL自己必須編譯成本地CPU指令,需要配置設定更多的不可共享記憶體和需要額外的CPU時間工作。
相信我,因為我自己是以C/C++背景接觸CLR,我是十足的懷疑論者并關注這些額外的開銷。事實是在運作時發生的第二次編譯階段并不損傷性能,它也不配置設定動态記憶體。無論如何,微軟為了保持額外的開銷最小已經在性能上做了很多工作。
假如你也是懷疑論者,你當然應該建幾個應用程式測試性能。此外,你應該運作幾個微軟或者其它生成的不凡的托管應用程式,并測試它們的性能。你想你應該會驚訝的發現實際性能怎麼會那麼好。
你可能覺得這難以置信,但是很多人(包括我)認為托管應用 程式勝過非托管程式。有很多原因證明這個。比如,運作時當JIT編譯器編譯IL代碼成本地代碼,編譯器比非托管代碼編譯器知道更多執行環境。這裡是一些托管代碼可以勝過非托管代碼的方法:
*JIT編譯器可以判斷應用程式是否可以在Intel奔騰4CPU上運作,利用奔騰4提供的特殊指令生成本地代碼。通常,非托管應用程式是為最小公分母CPU編譯的并避免使用特殊的指令那将給應用程式一個性能提升。
*JIT編譯器可以運作時判斷某個測試總是false。列如,考慮一個方法包含以下代碼。
這些代碼可以引起JIT編譯器不生成任何CPU指令假如主機上隻有一個CPU。假如這樣,對于主機,本地代碼将會微調;結果代碼将會更小然後執行的更快。if(numberOfCPUs>1) { ... }
*CLR可以給出代碼的執行輪廓和重編譯IL成本地代碼當應用程式運作時。依賴于觀察執行模型預報,重編譯代碼可以被重組減少錯誤分支。目前版本的CLR不做此工作,但是未來的版本可能會。
這些是幾個原因使你期待未來的托管代碼比今天的非托管代碼執行的更好。正如我所說,對于大多數應用程式現在已經有足夠好的性能,并且随着時間的推進會有所改善。
假如你的實驗顯示出CLR的JIT編譯器沒有提供給你的應用程式需要的性能,你可能要利用裝載在.NET Framework SDK中的NGen.exe工具。這個工具編譯程式集中所有的IL代碼成本地并把結果儲存在一個檔案中存在磁盤上。在運作時,當程式集被加載完成,CLR自動檢查是否已經有一個程式集的預編譯版本存在,假如有,CLR加載預編譯代碼而不需要在運作時再編譯。注意NGen.exe必須儲存它關于實際執行環境的假設,為了這個原因,NGen.exe産生的代碼将不會像JIT編譯器生成的代碼一樣高度優化。在這一章稍後我将讨論更多關于NGen.exe的細節。
此外,你可以需要考慮使用System.Runtime.ProfileOptimization類。當你的應用程式運作時,這個類使CLR記錄(到一個檔案)JIT編譯了什麼方法。然後,未來你的應用程式啟動時,JIT編譯器将會同時用其他的線程編譯器這些方法假如你的應用程式是在一個多CPU的機器上運作。最後的結果是你的應用程式運作的更快,因為多個方法被同時編譯了,并且在應用程式初始化期間,代替原來使用者與你的應用程式互動時的實時編譯。
IL and Verification(中間語言和驗證)
IL是以堆棧為基礎的(stack-based),這意味着它的所有指令,都是把操作數都壓入一個執行堆棧,彈出結果出棧。因為IL沒有提供指令操作寄存器,人們很容易就可以建立一門新的語言并把它編譯産生的代碼以CLR為目标。
IL指令也是無類型的。列如,IL提供一個加(add)指令把壓入堆棧中的最後兩個操作數加起來。32位和64位版本的加指令沒有分别。當加指令執行時,它決定堆棧中的操作數類型并執行适合的操作。
在我看來,IL最大的好處不是抽離于CPU底層。IL的最大好處是提供給應用程式健壯性和安全性。當編譯IL成本地CPU指令時,CLR執行一個叫做verification的程序。Verification檢查進階IL代碼并確定代碼要做的事情是安全的。列如,verification檢查每一個被調用的方法都有正确的參數數目,每個參數傳到每個方法都有正确的類型,每個方法的傳回值使用得當,每個方法有一個傳回聲明,等等。托管子產品的中繼資料包括所有用于verification程序的方法和類型資訊。
在Windows中,每個程序都有自己的虛拟位址空間。分離的位址空間十分必要,因為你不能信任一個應用程式的代碼。一個應用程式完全可能(不幸的是,太常見了)讀或寫一個無效的記憶體位址。通過把各個Windows程序放到分離的位址空間,你可以增加健壯性和穩定性;一個程序不會嚴重影響另一個程序。
通過驗證托管代碼,無論如何,你知道代碼不适合通路記憶體和不會影響另一個應用程式的代碼。這意味着在一個Windows的虛拟位址空間你可以運作多個托管應用程式。
因為Windows多個程序需要很多作業系統資源,持有多個程序會損耗性能和限制資源可用性。通過在一個作業系統程序中運作多個應用程式減少程序數量可以改善性能,需要更少的資源,并像每個應用程式持有程序一樣穩健。這是托管代碼的另一個好處和非托管代碼相比。
實際上CLR做的,是提供使多個托管應用程式在一個作業系統程序執行的能力。每個托管應用程式在一個AppDomain裡執行。預設的,每個托管EXE檔案将會在它自己的分離位址空間運作,并隻有一個AppDomain。然而,一個持有CLR的程序(比如IIS【Internet Information Service】或者Microsoft SQL Server)可以決定在一個作業系統程序中運作多個AppDomain。
Unsafe Code(不安全代碼)
預設的,Microsoft的C#編譯器生成安全代碼。安全代碼是指驗證安全的代碼。然而,Microsoft的C#編譯器運作開發者寫不安全的代碼。非安全代碼運作直接在記憶體位址運作并可以在這些位址中操作位元組。這是一個非常強大的特點并且是常用的,當你和非托管代碼互操作或者當你想要改變時序要求嚴格的算法性能。
無論如何,使用非安全代碼介紹了一個重大風險:非安全代碼會破壞資料結構并利用或甚至打開安全弱點。由于這個原因,C#編譯器要求所有包含非安全代碼的方法标記unsafe關鍵字。此外,C#編譯器需要你打開/unsafe編譯器開關編譯源碼。
當JIT編譯器嘗試編譯一個非安全代碼,它要檢查程式集的方法是否已經被System.Security.Permissions.Security Premission通過System.Security.Permissions.SecurityPermissionFlag’s SkipVerfication設定标記。假如這個标記已經設定了,JIT編譯器将會編譯非安全代碼并允許執行。CLR信任這些代碼并希望直接位址和位元組操作不會引起任何傷害。假如這個标記沒有設定,JIT編譯器抛出一個System.InvalidProgramException或一個System.Security.VerificationException錯誤,阻止方法執行。實際上,整個應用程式将會在這個點停止,但是至少不會造成任何傷害。
注意 預設的,從本地機器或者通過network(多台計算機的連接配接的網絡)分享的程式集是被信任的,意味着它們可以做任何事,包括執行非安全代碼。然而,預設的,通過Internet網絡下載下傳的程式集是不會被授權執行非安全代碼的。假如它們包含非安全代碼,上述提到的異常就會被抛出。一個管理者/最終使用者可以改變這些預設設定;無論如何,管理對代碼的行為負有全責。 |
微軟支援一個叫做PEVerify.exe的工具,它檢查一個程式集内的所有方法并通知你任何包含有非安全代碼的方法。你可能需要考慮運作PEVerify.exe檢查你引用的程式集;這将讓你知道你從區域網路或者網際網路下載下傳的應用程式在運作時是否有問題。
你應該意識到驗證需要通路任何包含在依賴程式集中的中繼資料。是以當你使用PEVerify檢查一個應用程式,它必須可以定位和加載所有的引用程式集。因為PEVerify使用CLR定位依賴程式集,通過使用相同的綁定和探索規則定位程式集 ,當運作時應該可以正常執行程式集。我将會在第二和第三章讨論這些綁定和探索規則,“Shared Assemblier and Strongly Named Assemblies.(分享程式集和強命名程式集)”
IL and Protecting Your Intellectual Property(IL和保護你的知識産權) 有的人關心IL沒有提供足夠的知識産權屬性去保護他們的算法。換句話說,他們認為你生成一個托管子產品,某個人也可以使用一個工具,比如IL Disassembler(IL 反彙程式設計式),可以簡單的反向工程确定你的代碼做了什麼。 是的,這是事實,IL代碼比起其它的程式集語言是更進階,并且,一般而言,反向工程IL代碼相對簡單。然而,當實作服務端代碼(比如網頁服務,網頁表格,或者存儲過程),你的程式集存在你的伺服器上。因為除了你公司人的人以外沒人可以通路程式集,除了你公司的人沒人可以使用任何工具檢視IL——你的知識産權完全安全。 假如你關注任何你釋出的程式集,你可以擷取一個混淆工具從第三方廠商。這些工具把所有的私有标記的名字混淆在你的程式集中繼資料中。它将變的十分困難假如某個人要縷清名字和弄懂每個方法的目的。注意這些混淆器隻能提供一點點保護因為IL必須在一些點上對CLR有用,JIT才能編譯IL。 假如你不認為混淆器沒有提供你想要的知識産權保護,你可以考慮使用一些非托管子產品實作更多的敏感算法,非托管子產品将會包含本地CPU指令替代IL和中繼資料。然後你可以使用CLR的互相操作特征(假設你又足夠的權限),應用程式的托管部分和非托管部分就會通信。當然,這是假設你不擔心在你的非托管代碼中人們會反向工程本地CPU指令。 |
The Native Code Generator Tool:NGen.exe(本地代碼生成工具:NGen.exe)
NGen.exe工具裝載在.NET Framework上,使用者在安裝應用程式時,NGen.exe會把IL代碼編譯成本地代碼。因為代碼是在安裝時編譯,CLR的JIT編譯不用再運作時編譯IL代碼,是以改善了應用程式的性能。NGen.exe工具在以下兩個場景會顯得有趣:
*改善一個應用程式的啟動時間 運作NGen.exe可以改善啟動時間是因為代碼已經被編譯成本地代碼是以編譯不會在運作時發生。
*減少一個應用程式的工作集 假如你相信一個程式集将會同時加載到多個程序,在程式集上運作NGen.exe會減少應用程式的工作集。原因是NGen.exe工具把IL代碼編譯成本地代碼并把輸出儲存到一個分離的檔案中。這個檔案可以同時記憶體映射到多個程序的位址空間 ,運作代碼分析;而不是需要每個需要的程序拷貝一份代碼。
在一個應用程式或一個單程式集上,當一個設定程式調用NGen.exe,應用程式的所有程式集或某個指定的程式集的IL代碼會被編譯成本地代碼。NGen.exe建立一個新的隻包含本地代碼的程式集檔案替代IL代碼。這個新的檔案放在一個檔案夾下,目錄名字像%SystemRoot%\Assembly\NativeImages_v4.0.####_64。這個目錄名字包括CLR版本和是否本地代碼編譯成32位或者64位版本Windows的訓示資訊。
現在,無論何時CLR加載一個程式集檔案,CLR查找是否有符合NGen的本地檔案已經存在。如果找不到本地檔案,CLR JIT編譯器像往常一樣編譯IL代碼。然而,如果存在符合的本地檔案,CLR将會使用包含在本地檔案中的編譯過的代碼,檔案中的方法也不會在運作時編譯。
表面上,這聽起來相當不錯!它聽起來好像你獲得了托管代碼的所有好處(垃圾回收,驗證,類型安全,等等)并且托管代碼完全沒有性能問題(JIT編譯)。然而,真實的狀況是,它不像第一眼看上去那樣美好。關于NGen的檔案有幾個潛在的問題:
*沒有知識産權保護 很多人相信它有可能裝運不包含IL代碼的NGen檔案,進而保護他們的知識産權。不幸的是,這不可能。在運作時,CLR要求通路程式集的中繼資料(比如實作反射或系列化的功能);這要求程式集包含IL和裝運了中繼資料。此外,假如CLR因為某些原因(接下來有所描述)不能使用,CLR将優雅的傳回JIT編譯程式集IL代碼,這樣一定可行。
*NGen檔案可以擺脫同步 當CLR加載NGen檔案,關于之前編譯好的代碼和目前環境,CLR将會比較大量的特征。假如有任何一點特征不比對,NGen檔案就不能使用,這時進入正常的JIT編譯器程序。這裡是部分必須比對的特征清單:
-CLR 版本:更新檔或服務包改變時CLR版本也将改變。
-CPU 類型:假如你更新你的處理器硬體CPU類型将會改變。
-Windows作業系統版本:一個新的服務包更新Windows系統版本也會更新。
-程式集的身份子產品版本ID( module version ID,MVID):當重新編譯後子產品版本ID将改變。
-引用程式集的版本ID:當你重新編譯一個引用的程式集時改變。
-安全:當你取消那些之前授權過的許可(比如聲明繼承,聲明link-time,SkipVerification,或者UnmanagedCode 許可)會引起安全改變。
注意 有可能在更新模型中運作NGen.exe。這告訴工具在以前已經是NGen檔案的所有程式集中運作NGen.exe。無論何時一個最終使用者安裝一個新的.NET Framework服務包,服務包的安裝程式都會在更新模型中自動運作NGen.exe以便NGen檔案可以和安裝的CLR版本保持同步。 |
*低下的執行時間性能 當編譯代碼時,NGen不能像JIT編譯器一樣做多個關于執行環境的假定。這是因為NGen.exe産生低質的代碼。列如,NGen不會優化特定CPU指令的使用;它為通路靜态字段增加了迂回因為靜态字段的實際位址隻有在運作時才知道。NGen到處插入代碼以調用類構造器因為它不知道代碼将會以什麼順序執行并且不知道類構造器是否已經被調用過。(見第8章,“Methods”,更多關于類構造器的内容。)一些 NGen的應用程式執行比它們的JIT編譯副本實際慢大概5%。是以,假如你考慮使用NGen.exe提升你的應用程式性能,你應該比較NGen和非NGen版本確定NGen版本實際上運作并不慢!對于一些應用程式來說,減小工作集提高性能,那麼使用NGen是極大的優勢。
歸功于剛才列出的所有問題,當你考慮NGen.exe你應該很謹慎。對于服務端應用程式,NGen.exe作用很小或者沒有用作因為僅在用戶端第一次請求時會體驗到性能損失;以後的用戶端請求都将高速運作。此外,對于大多數服務應用程式,代碼隻會執行個體化一次,是以沒有工作集的好處。
對于用戶端應用程式,NGen.exe可能有意義對于改善啟動時間或減少工作集假如一個程式集被同時用于多個應用程式。甚至在程式集不用于多個應用程式的執行個體中,在程式集中執行NGen可以改善工作集。此外,假如NGen.exe用于所有的用戶端應用程式的程式集,CLR完全不需要加載JIT編譯器,進一步減少了工作集。當然,假如一個程式集不是NGen的或者假如一個程式集的NGen檔案不可用,那麼将加載JIT編譯器,應用程式的工作集也就變大了。
對于大型用戶端應用程式會體驗一個很長的啟動時間,Microsoft提供一個管理檔案導向優化工具(Managed Profile Guided Optimization,MPGO.exe)。這個工具分析執行你的應用程式時需要啟動什麼。為了優化生成本機映像這個資訊将會回報給NGen.exe工具。這允許你的應用程式啟動更快并減少工作集。當你準備裝運你的應用程式,憑借MPGO工具啟動它然後操練你的應用程式的公共任務。你的執行部分的代碼資訊被寫入一個檔案,它嵌入在你的程式集檔案中。NGen.exe工具使用這個檔案資料更好的優化NGen.exe生成的本機映像。
The Framework Class Library(Framework類庫)
.NET Framework包含架構類庫(Framework Class Library,FCL)。FCL是一個DLL程式集集合包含幾千個類型,在每個類型中暴露了幾個功能函數。Microsoft也生成附加的庫比如Windows Azure SDK和DirectX SDK。這些附加的庫給你的使用提供了更多的類型,暴露了更多的功能函數。實際上,Microsoft生成類庫的速度驚人,使類庫前所未有的容易當開發者使用各種各樣Microsoft技術時。
這裡是一些種類的應用程式,開發者建立可以通過這些程式集:
*網頁服務(Web services) 方法可以加工資訊很容易的發送到網際網路通過使用微軟的ASP.NET XML Web Service技術或微軟的Windows Communication Foundation(WCF)技術。
*網頁表單/MVC 以HTML為基礎的應用程式(網站) 典型的,ASP.NET應用程式将會做資料庫查詢和網頁服務調用,合并和過濾傳回的資訊,然後通過豐富的以HTML為基礎的使用者界面在一個浏覽器上呈現資訊。
*Rich Windows GUI 應用程式 代替使用網頁建立你的應用程式使用者界面,你可以使用Windows商店提供的更強大的,更高性能的功能,Windows Presentation Foundation(WPF),或者Windows Forms技術。GUI應用程式可以利用控件、菜單、和觸摸、滑鼠、觸控筆和鍵盤事件的好處,GUI應用程式可以直接和作業系統底層交換資訊。Rich Windows應用程式也可以做資料庫查詢和使用網頁服務。
*Windows console應用程式 對于簡單UI要求的應用程式,一個console應用程式提供了快捷簡單的方法生成一個應用程式。編譯器,工具都是典型的作為console應用程式實作的。
*Windows services 是的,通過使用.NET Framework的Windows Service Control Manager(SCM)有可能生成一個可控的服務應用程式。
*資料庫存儲過程(Database stored procedures) 微軟的SQL Server,IBM的DB2,和Oracle的資料庫服務運作開發者通過使用.NET Framework寫他們自己的存儲過程。
*元件庫(Component library) .NET Framework允許你生成獨立的程式集(元件),程式集(元件)包含的類型可以容易的合并到任何之前提到的應用程式類型中。
重要 Visual Studio允許你建立一個Portable Class Library(可移植類庫)項目。這個項目類型讓你建立一個單獨的類庫程式集可以在各種應用程式類型工作,包括.NET Framework本身,Silverlight,Windows Phone,Window Store應用和Xbox360。 |
因為FCL不誇張的包含成千上萬的類型,在一個命名空間裡有一個相關類型的集合呈現給開發者。列如,System命名空間(你應該變的很熟悉的一個命名空間)包含基礎類型Object,其他的所有類型都繼承了Object。此外,System命名空間包含的類型還有integers,characters,strings,exception handling,和console I/O一串資料類型之間的安全轉換工具類型,轉換資料類型,生成随機數,和執行各種數學功能。所有的應用程式都将使用System命名空間下的類型。
要通路架構的任何特性,你需要知道哪個命名空間包含類型暴露的功能函數之後。很多類型允許你自定義他們的行為;那你可以通過簡單的從你需要的FCL類型繼承到你自己的類型。平台的面向對象本質就是.NET Framework怎麼樣呈現一緻的程式設計範式給開發者。同時,開發者可以容易的建立包含他們自己類型的命名空間。這些命名空間和類型無縫的合并到程式設計範式裡。和Win32程式設計範式相比,這種新的方式極大的了簡化了軟體開發。
大多數在FCL呈現的類型的命名空間可以在任何種類的應用程式中使用。下表列出了一些更加一般的命名空間和簡單描述,在那個命名空間什麼類型被使用了。這是可用的命名空間很小的樣本。随着微軟日益增長的命名空間集合,請看檔案伴随着各種微軟SDK以增加熟悉度。
這本書是關于CLR并和CLR緊密互相作用的一般類型。是以這本書的内容适用于所有寫應用程式的開發者或者以CLR為目标的元件。存在其它很多好書,涉及指定應用程式類型比如Web Services,Web Forms/MVC,Windows Presentation Foundation,等等。這些書在幫助你生成你的應用程式方面會給你一個很好的開始。我傾向于認為這些指定應用程式的書幫助你自上而下的學習因為它們專注于應用程式類型而不是開發平台。在這本書中,我将拿出資訊幫助你自下而上的學習。在你讀了本書和一本指定應用程式的書後,你應該可以簡單熟練的建立任何種類你想要的應用程式。
The Common Type System(公共類型系統)
目前為止,很明顯CLR都是關于類型的。類型暴露功能函數給你應用程式和其它類型。類型是通過一種程式設計語言寫的代碼可以和不同的程式設計語言談話的機制。因為是CLR的根本,微軟建立了一個形式規範——公共類型系統(Common Type System,CTS)——描述了類型怎樣定義和它們怎樣運作。
注意 實際上,微軟已經送出了CTS做為.NET Framework的其它部分,包括檔案轉換,中繼資料,IL和通路平台低層(P/Invoke)達到ECMA(歐洲計算機制造聯合會)為了實作标準化目标。标準叫做公共語言基礎設施(Common Language Infrastructure,CLI)并且是ECMA-335規格。此外,微軟還送出了FCL部分,C#程式設計語言(ECMA-334),和C++/CLI 程式設計語言。關于這些工業标準的資訊,去ECMA網站檢視,屬于技術委員會39(Technical Committee 39,TC39):http://www.ecma-international.org。你也可以參考微軟自己的網站:http://msdn.microsoft.com/en-us/netframework/aa569283.aspx 。此外,微軟已經對ECMA-334和ECMA-335規格應用了他們的社群承諾。 |
CTS規格聲明了一個類型可以包含0個或多個成員。在第二部分,“設計類型(Designing Types)”,我會很詳細的講述所有成員。現在,我隻是給你簡單的介紹一下它們:
*字段(Field) 一個資料變量,是對象聲明的一部分。字段通過它們的名字和類型被識别為字段。
*方法(Method) 一個功能,在對象中執行一個操作,經常改變對象的聲明。方法有一個名字,一個簽名,和修飾詞。簽名指定參數的數量(和它們的順序),參數的類型,方法是否有傳回值,如果這樣,方法傳回值的類型。
*屬性(Property) 對于調用者,這個成員看上去像是一個字段。但是對于類型實作者,它像一個方法(或者兩個方法)。必要時,屬性允許一個實作者驗證輸入參數和對象聲明在通路數值和/或計算一個數值之前。它們也允許類型使用者有簡單的文法。最後,屬性允許你建立隻讀或隻寫的“字段”。
*事件(Event) 一個事件允許一個通知機制在一個對象和其它感興趣的對象之間。例如,一個按鈕可以拿出一個事件通知其它的對象當按鈕被點選的時候。
CTS也制定類型可見度和類型成員通路規則。例如,把一個類型定義為公共的(叫做public)輸出類型,使它可見和可通路對于任何程式集。在另一方面,把一個類型當作程式集(在C#中調用internal),代碼可見和可通路都隻能在同一程式集中。是以,CTS通過程式集為類型形成的可見邊界建立規則,CLR實施可見規則。
一個調用者可見的類型可以進一步限制調用者通路類型成員的能力。下面的清單顯示了控制通路一個成員的有效選項:
*Private 隻有在同一個類類型中的其它成員才可以通路該成員。
*Family 派生類型可以通路該成員,不管它們是不是在同一個程式集中。注意,很多語言(比如C++和C#)把family稱作protected。
*Family and assembly 派生類型可以通路該成員,但是隻有派生類型定義在同一個程式集中。很多語言(比如C#和Visual Basic)不提供這個通路控制。當然,IL彙編語言提供這個通路控制。
*Assembly 同一個程式集中的任何代碼都可以通路該成員 。很多語言把assembly稱作internal。
*Family or assembly 在任何程式集中派生的類型都可以通路該成員。在同一個程式集中的任何類型都可以通路該成員。C#把family or assembly稱作protected internal。
*Public 任何程式集中的任何代碼都可以通路該成員。
此外,CTS定義規則控制類型繼承、虛拟方法、對象生命周期等等。這些規則已經被設計出來适應當代程式設計語言的語義表達。實際上,你不需要學習CTS規則本身因為你選擇的語言将會使用相同的方式暴露它自己的文法和類型規則。當編譯區間它釋出程式集時,它會把語言特定的文法映射成IL,CLR的“語言”。
當我第一次用CLR工作時,我馬上發覺它是考慮的最好的語言,把代碼的行為作為兩個單獨和獨特的東西。使用C++/CLI,你可以定義你自己的類型和類型的成員。當然,你也可以使用C#或者Visual Basic定義相同的類型和類型成員。的确,你定義類型的文法依賴于你選擇的語言而不同,但是類型的行為将會完全一樣忽略使用的語言因為CLR的CTS定義類型的行為。
為了幫助理清這個概念,讓我給你一個示例。CTS允許一個類型隻能從一個基類繼承。是以,即使C++語言支援類型從多個基類繼承,CTS不接受和操作任何這樣的類型。為了幫助開發者,微軟的C++/CLI編譯器會報告一個錯誤假如它發現你試圖建立包含一個類型繼承多個基類的托管代碼。
這裡是其他的CTS規則。所有的類型必須(最終)必須繼承自一個預先定義好的類型:System.Object。正如你所看到的,Object是一個類型的名字,這個類型定義在System命名空間中。這個Object是其它所有類型的根類型是以確定了每個執行個體化類型都有一個最小的行為集合。明确的,System.Object類型允許你做以下的事情:
*比較兩個執行個體是否相等。
*為執行個體擷取哈希碼。
*擷取一個執行個體的類型。
*為目前執行個體建立一個淺副本。
*擷取一個表示目前執行個體的字元串。
The Common Language Specification(公共語言規格)
COM允許用不同的語言建立可以互相通信的對象。另一方面,CLR現在內建了所有語言并允許一種語言建立的對象和另一種完全不一樣的語言代碼寫的作為平等公民對待。這種內建成為可能是因為CLR的類型、中繼資料(類型自描述資訊)和公共執行環境的标準集合。
即便這個語言內建是一個漂亮的進球,事情的真相是各個程式設計語言之間有很大的差別。例如,一些語言大小寫不敏感,一些不提供無符号整數,操作符重載,或者方法支援可變數量的參數。
假如你傾向于建立一個其它程式設計語言也能容易的通路的類型,你需要使用你的程式設計語言僅有的特征確定在其它語言都有效。為了幫助你實作這個,微軟定義了一個公共語言規格(Common Language Specification,CLS)細節給編譯器供應商,他們的編譯器必須支援最小特征集合假如它們的編譯器要生成的類型相容其它元件,這些元件是通過在CLR上符合公共語言規範(CLS-compliant)的語言寫的。
CLR/CTS支援的特征比在CLS子集中定義的特征多,是以假如你不關心不同語言間的可操作性,你可以開發很多類型且隻限于語言的特征集合。确切的,外部可見的類型和方法必須追随CLS定義的規則假如它們可以被任何符合公共語言規範(CLS-compliant)的開發語言通路。注意CLS規則不适用于隻在程式集内的可通路性。下圖總結了這一點表達的觀點。
如下圖所示,CLR/CTS提供了一個特征集合。一些語言暴露CLR/CTS一個大的子集。例如,一個開發者将使用IL彙編語言寫代碼,他可以使用CLR/CTS提供的所有特征。其他的大多數語言,比如C#,Visual Basic,和Fortran,暴露了CLR/CTS的一個子集給開發者。CLS定義的最小特征集合所有語言都必須支援。
假如你隻用一種語言設計類型,并希望類型可以被其它語言使用,你不能在類型的public和protected成員中利用CLS之外的任何特征。這麼做意味着你的類型成員不能被開發者用另一種語言寫的代碼通路。
接下來的代碼中,一個符合公共語言規範(CLS-compliant)的類型将定義在C#中。但是,類型包含幾個不符合公共語言規範(non-CLS-compliant)的構造引起C#編譯器抱怨代碼。
using System;
[assembly:CLSCompliant(true)]namespace SomeLibrary{ public sealed class SomeLibraryType
{ // warning CS3002: “SomeLibrary.SomeLibraryType.Abc()”的傳回類型不符合 CLS
public UInt32 Abc()
{ return 0;
} //warning CS3005: 僅大小寫不同的辨別符“SomeLibrary.SomeLibraryType.abc()”不符合 CLS
public void abc()
{
} //no warning:這個方法是私有的
private UInt32 ABC()
{ return 0;
}
}
}
在這份代碼中,[assembly:CLSCompliant(true)]屬性被應用于程式集。這個屬性告訴編譯器以確定任何公開暴露沒有任何構造的類型将會阻止類型被任何其它程式設計語言通路。當代碼被編譯後,C#編譯器釋出兩個警告。第一個警告報告了因為方法Abc傳回一個無符号整數。一些其它的程式設計語言不能操作無符号整數值。第二個警告是因為這個類型暴露兩個公共方法隻有大小寫和傳回類型不同。Visual Basic和其他的一些語言不能同時調用這些方法。
有趣的是,假如你删除了sealed class SomeLibraryType前的public并重新編譯,兩個警告都會消失。原因是SomeLibraryType将會預設為internal進而程式集不再暴露在外面。完整的CLS規則清單,參考在.NET Framework SDK文檔中的“跨語言互操作性(Cross-Language Interoperability)”章節(https://msdn.microsoft.com/zh-cn/library/730f1wy3.aspx)。
讓我把CLS規則很簡單的提取出來。在CLR中,類型的每個成員要麼是個字段(資料)要麼是個方法(行為)。這意味着每個程式設計語言都能通路字段和調用方法。一般的字段和一般的方法用于特殊或常用的途徑。為了減少程式設計,語言通常提供額外的抽象使編寫這些常用程式設計模式更簡單。例如,語言暴露的思想比如枚舉、數組、屬性、索引器、委托、事件、構造器、終結器、運算符重載、轉換操作符等等。當一個編譯器在你的源碼中遇到這些中的任何一個時,編譯器必須把這些構造轉換成字段和方法,這樣CLR和其它的程式設計語言才能通路構造。
考慮下面的類型定義,它包含一個構造器,一個終結器,一些運算符重載,一個屬性,一個索引器和一個事件。注意在在這的代碼隻是為了能夠編譯;而不是正确的實作類型的方式。
using System;namespace SomeLibrary
{ internal sealed class Test
{ //構造器
public Test() { } //終止器
~Test() { } //操作符重載
public static Boolean operator ==(Test t1, Test t2)
{ return true;
} public static Boolean operator !=(Test t1, Test t2)
{ return false;
} //一個操作符重載
public static Test operator +(Test t1, Test t2)
{ return null;
} //一個屬性
public String AProperty
{ get { return null; } set { }
} //一個索引器
public String this[Int32 x]
{ get { return null; } set { }
} //一個事件
public event EventHandler AnEvent;
}
}
當編譯器編譯代碼時,結果是包含有幾個字段和方法的類型。你可以使用.NET Framework SDK提供的IL反彙編工具(ILDasm.exe)檢查生成的托管子產品,如下所示。
下表顯示了程式設計語言構造怎麼等價映射到CLR字段和方法。
Test類型的字段和方法(從中繼資料中擷取)
類型成員 | 成員類型 | 等價程式設計語言構造 |
AnEvent | 字段 | 事件;字段的名字是AnEvent,它的類型是System.EventHandler。 |
.ctor | 方法 | 構造器。 |
Finalize | 終止器。 | |
add_AnEvent | Event添加通路器方法。 | |
get_AProperty | Property擷取通路器方法。 | |
get_Item | 索引器擷取通路器方法。 | |
op_Addition | +操作符。 | |
op_Equality | ==操作符。 | |
op_Inequality | !=操作符。 | |
remove_AnEvent | Event移除通路器方法。 | |
set_AProperty | Property設定通路器方法。 | |
set_Item | 索引器設定通路器方法。 |
Test類型下額外的節點在上表中沒有提及——.class、.custom、AnEvent、AProperty和Item——識别出類型的附加中繼資料。這些節點不映射到字段和方法;它們隻是提供了一些關于類型的額外資訊給CLR、程式設計語言或者工具通路。例如,一個工具可以看出Test類型提供了一個事件,叫做AnEvent,通過兩個方法(add_AnEvent和remove_AnEvent)暴露給外面。
Interoperability with Unmanaged Code(和非托管代碼的互操作性)
比起其他的開發平台.NET Framework提供了大量的優勢。然而,很少有公司可以負擔得起重新開機設計和重新實作他們現在已有的代碼。微軟察覺了這一點并構造了CLR,它提供了一個允許應用程式由托管和非托管兩部分組成的機制。特定的,CLR支援三個互操作性場景:
*托管代碼可以調用一個DLL中包含的非托管功能函數 托管代碼通過使用一個叫做P/Invoke的機制調用包含在DLL集合中的功能函數。畢竟,FCL内部定義了很多類型,從Kernel32.ll、User32.dll等等調用暴露的功能函數。很多程式設計語言暴露一個機制使托管代碼調用包含在DLL中的功能函數很容易實作。例如,一個C#應用程式可以調用Kernel32.dll暴露的CreateSemaphore功能函數。
*托管代碼可以使用一個存在的COM元件(服務) 很多公司已經實作了大量的非托管COM元件。使用在這些元件中的類型庫,描述COM元件的一個托管程式集将會建立。托管代碼可以通路托管程式集中的類型就像通路其它的托管類型一樣。查詢裝載在.NET Framework SDK中的Tlblmp.exe了解更多資訊。有時,你可能沒有一個類型庫或者對于Tlblmp.exe産生的内容你可能想要更多的控制權。如果這樣,你可以手動在源碼中建立一個類型,CLR可以用于擷取合适的互操作性。例如,你可以在一個C#應用程式中使用DirectX COM元件。
*托管代碼可以使用一個托管類型(服務) 很多已經存在的非托管代碼要求你供給一個COM元件使代碼能正确工作。使用托管代碼實作這些元件容易得多,你可以避免所有代碼都要引用計數和接口。例如,你可以使用C#建立一個ActiveX控件或外殼擴充程式。檢視裝載.NET Framework SDK中的TlbExp.exe和RegAsm.exe工具了解更多資訊。
注意 微軟現在為類型庫導入器(Type Library Importer)工具提供源碼和P/Invoke互操作助手(Interop Assitant)幫助開發者和本地代碼互動。這些工具和源碼可以從http://CLRInterop.CodePlex.com/ 下載下傳。 |