微軟.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(+)文件包含了一塊數據被稱作載貨單。載貨單是元數據表其中一個簡單的集合。這些表描述了文件如何組成程序集合,公開導出類型實現的文件在集合中,並且資源或者數據文件與程序集合都有關聯。
注意 使用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指令。
第一次調用一個方法
if(numberOfCPUs>1) { ... }
這些代碼可以引起JIT編譯器不生成任何CPU指令假如主機上只有一個CPU。假如這樣,對於主機,本地代碼將會微調;結果代碼將會更小然後執行的更快。
*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和保護你的知識產權)
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的文件有幾個潛在的問題:
注意 有可能在更新模型中運行NGen.exe。這告訴工具在以前已經是NGen文件的所有程序集中運行NGen.exe。無論何時一個最終用戶安裝一個新的.NET Framework服務包,服務包的安裝程序都會在更新模型中自動運行NGen.exe以便NGen文件可以和安裝的CLR版本保持同步。*沒有知識產權保護 很多人相信它有可能裝運不包含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不能像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技術時。
這裡是一些種類的應用程序,開發者創建可以通過這些程序集:
重要 Visual Studio允許你創建一個Portable Class Library(可移植類庫)項目。這個項目類型讓你創建一個單獨的類庫程序集可以在各種應用程序類型工作,包括.NET Framework本身,Silverlight,Windows Phone,Window Store應用和Xbox360。*網頁服務(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允許你生成獨立的程序集(組件),程序集(組件)包含的類型可以容易的合並到任何之前提到的應用程序類型中。
因為FCL不誇張的包含成千上萬的類型,在一個命名空間裡有一個相關類型的集合呈現給開發者。列如,System命名空間(你應該變的很熟悉的一個命名空間)包含基礎類型Object,其他的所有類型都繼承了Object。此外,System命名空間包含的類型還有integers,characters,strings,exception handling,和console I/O一串數據類型之間的安全轉換工具類型,轉換數據類型,生成隨機數,和執行各種數學功能。所有的應用程序都將使用System命名空間下的類型。
要訪問框架的任何特性,你需要知道哪個命名空間包含類型暴露的功能函數之後。很多類型允許你自定義他們的行為;那你可以通過簡單的從你需要的FCL類型繼承到你自己的類型。平台的面向對象本質就是.NET Framework怎麼樣呈現一致的編程范式給開發者。同時,開發者可以容易的創建包含他們自己類型的命名空間。這些命名空間和類型無縫的合並到編程范式裡。和Win32編程范式相比,這種新的方式極大的了簡化了軟件開發。
大多數在FCL呈現的類型的命名空間可以在任何種類的應用程序中使用。下表列出了一些更加一般的命名空間和簡單描述,在那個命名空間什麼類型被使用了。這是可用的命名空間很小的樣本。隨著微軟日益增長的命名空間集合,請看文件伴隨著各種微軟SDK以增加熟悉度。
注意 實際上,微軟已經提交了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定義的最小特征集合所有語言都必須支持。
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)檢查生成的托管模塊,如下所示。
類型成員 成員類型 等價編程語言構造 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支持三個互操作性場景:
注意 微軟現在為類型庫導入器(Type Library Importer)工具提供源碼和P/Invoke互操作助手(Interop Assitant)幫助開發者和本地代碼交互。這些工具和源碼可以從http://CLRInterop.CodePlex.com/ 下載。*托管代碼可以調用一個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工具了解更多信息。