.NET框架基礎知識(1)
參考資料:
- http://www.tracefact.net/CLR-and-Framework/DotNet-Framework.aspx (非常經典的一篇文章)
- 精通C# (第六版)
- CLR via C# (第三版)
1 術語
面試出現頻率:從來沒人問過。事實上我都不知道怎么問,考背書嗎?倒是可以問問知不知道現在.NET最新版本是什么,考察面試者是否對新技術足夠敏感。
重要程度:3/10
需要理解的程度:知道這些縮寫(CLR,BCL,FCL,CTS,CLS)各代表什么即可。仔細讀一遍http://www.tracefact.net/CLR-and-Framework/DotNet-Framework.aspx
1.1什么是.NET框架?在各個平台版本中,有什么值得強調的更新?
.NET框架是以一種采用系統虛擬機(即CLR)運行的,面向CLR的編程平台,以CLR為基礎。.NET的基礎類庫運行於CLR之上(類比Java的虛擬機),作為其他各種功能的基石。.NET框架支持多種語言(C#、F#、VB.NET、C++、Python等)的開發。它的前身是Windows DNA。現在.NET框架的擴展性甚至超過Java,其的Mono為Mac OS提供了支持,Xamarin可媲美安卓開發,可以在任何手機上開發。
.NET框架是開源的。它的代碼在https://github.com/dotnet/。如果你的commit有幸被接受,即使改動有多么微小,也是無上的榮耀,你絕對應該把它寫到你簡歷的第一行,這個成就可以和“為Linux內核優化做過貢獻”相比,那可比曾經在BAT做過幾年的經歷牛逼多了。
所有.NET支持的語言編寫出來的程序,在支持.NET的編譯器編譯之后,會先產出程序集,其主要內容是IL和元數據。之后,JIT再將其翻譯為機器碼。
甲骨文公司的Java EE是.NET平台的競爭對手之一。
.NET框架現在已經出到了版本4.6.1。在3.0之前,.NET框架的Web解決方案是ASP.NET(Webform & MVC),數據庫連接為ADO.NET(支持過去的ODBC,OLE等,並支持SQL Server和Oracle),Windows Form則作為Windows下的應用解決方案。
.NET最重大的一個版本更新是3.0,其中,提出了WCF(統一了過去Web服務混亂的形式,形成了一個統一的格式,並采用SOAP),WPF(作為Windows form的增強版)以及WF。
.NET3.5集成了LINQ。另外Entity Framework取代ADO.NET,它對應VS2008。
.NET4.0提出了任務並行庫和PLINQ。
.NET 5 (即.NET Core 1.0)在2016年6月27日推出。是次推出伴隨着ASP.NET Core (即ASP.NET 6)和Entity Framework 7。這些產品將支持Windows,OS X和Linux三種操作系統。
新版本的.NET項目使用.json文件代替了過去的.xxproj,.sln和.suo文件,這符合目前的主流,即用json代替XML。新版本的.NET框架要傳輸給我們的理念是:這是一個跨平台的,開源的框架。一切都是依賴注入,一切都是nuget,開發徹底組件化,能解耦的全都解耦。ASP.NET Core徹底擺脫了System.Web這個頑疾,在其中,我們甚至連MVC都是注入進去的。如果想得到什么組件,要么通過依賴注入,要么就使用nuget。永遠不要手動add reference,目前我知道的唯一的例外是System.Configuration。當你和團隊其他人並行開發系統的不同模塊時,你們可以用nuget互相得到對方模塊中的工程。Nuget相比add reference,更不容易出錯,界面更友好,且不會輕易陷入dll陷阱。
經過.NET牌編譯器編譯之后的程序集有兩種形態:類庫(.dll)形態和可執行文件(.exe)形態。.NET自帶了很多類庫,統稱為FCL。BCL是FCL的一個子集。
1.2 基礎類庫(BCL)
Base Class Library (BCL) 是微軟所提出的一組標准庫,可提供給.NET Framework所有語言使用。隨着 Windows 以及.NET Framework 的成長,BCL 已近乎成為在.NET上的 Windows API。mscorlib.dll程序集幾乎就是基礎類庫的代名詞。
當安裝.NET Framework時,所有的基礎類庫被部署到全局程序集緩存(GAC)。它的位置一般在C:\Windows\assembly。所以你不需要在你的工程中手動引用任何的基礎類庫,它們會被自動引用。如果你從GAC中刪除了mscorlib.dll,你的IDE將變成一個什么都不懂的白痴。因為沒有mscorlib.dll,意味着沒有基礎類庫,沒有整型,字符串,控制台…你什么都做不了。
部分mscorlib.dll包括的命名空間:
- System:.NET Framework 類庫中最基底的服務,提供應用程序域 (Application Domain),數據類型,I/O 以及其他類庫的基礎。
- System.Collections:提供非泛型數據結構以及集合對象的支持,其中 System.Collections.Generic中包括所有的泛型數據結構。
- System.Configuration:提供 .NET 應用程序在配置設置上的支持。
- System.Data:ADO.NET 的組成類庫,為數據訪問功能的核心功能。
- System.Drawing:提供 .NET 的繪圖能力,包含基本位圖處理以及視頻與色彩處理,打印支持也由本名字空間提供,此名字空間包裝了大多數的 GDI 以及 GDI+ 的 API。
- System.IO:提供數據流與文件讀寫的支持
- System.Net:.NET 中的網絡功能
- System.Reflection:反射
- System.Diagnostics:.NET 中提供系統診斷,除錯,追蹤與運行外部進程的能力
- System.ServiceModel:WCF 的組成類庫,於 .NET Framework 3.0 時出現。
- System.Text:對文字,編碼以及正規表達式的支持。
- System.Threading:線程控制
- System.Windows.Forms: Windows Forms 的組成類庫,包裝了 Win32 用戶界面,視窗,共用控件,以及 Shell 的基礎 API,以提供設計 Windows 應用程序用戶界面所需的支持。
- System.Windows:WPF 的組成類庫,於 .NET Framework 3.0 時出現。
- System.Web:ASP.NET 的組成類庫,令工程可以和 IIS 服務器交互,XML Web Service 開發的基本支持也由本類別提供。ASP.NET Core中消失(如果你不打算用IIS做服務器的容器,則你不需要這個類庫)。
- System.Xml:XML 解析器
- System.Linq,System.Xml.Linq:LINQ 的核心類庫,System.Linq 是 LINQ to Object,而 System.Xml.Linq 則是 LINQ to XML。
然而在C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\我們還有一個System.dll,這個參考是每次新建工程時VS自動引用的若干參考之一。這個程序集中也有一個System命名空間,它的內容和mscorlib.dll中的不同。可以看到,System這個命名空間存在於不止一個程序集中。這意味着不同的程序集可以共享一個命名空間。
在System.dll中,System類型擁有Uri這個成員,mscorlib.dll中System類型擁有int這個成員(基元類型)。所以我們可以做個試驗,如果我們將工程中對System的引用去掉,那么我們就不能定義一個Uri類型的對象。但我們仍然可以使用int類型,因為它雖然也在System這個類型里面,但位於mscorlib.dll中。當你去掉對System的引用時,你僅僅去掉了System.dll和里面的功能,但你沒有去掉mscorlib.dll中System類型的功能。
BCL是屬於整個.NET框架的,並非某種語言的一個基礎類庫。例如,C#的string類型的所有功能和定義來源於mscrolib.dll中的System.String,而VB的string類型的功能和定義也來源於相同的地方。基礎類庫中定義的類型稱為基元類型,它也是為.NET框架所有的語言共享。
在.NET Core中,BCL改名換姓變成了Corefx。源碼在https://github.com/dotnet/corefx。
1.3 框架類庫(FCL)
作為一名.NET程序員,每天都要打交道的就是FCL了(框架類庫)。BCL是FCL的一個子集。簡單來說FCL除了BCL的那部分,就是我們要引用的外部參考。
1.4 CTS(公共類型系統)和CLS(公共語言規范)
簡單的說,CTS就是說話的語法和規范。你可以理解為,英語是一種語言,英語的CTS(至少絕大一部分)就是“實用英語語法(張道真)”這本書。如果C#沒了語法,那就沒有class,沒有接口,變成了偽碼。
參考資料中的第一個鏈接講的很好,我就在這里總結一下吧:
- CTS是一套語法。類似“英語語法”。它規定了一套約束,例如英語規定所有的字詞都是由26個字母組成的(以及其他很多規則)。服從這套語法的語言都可以被看成是英語的某種方言,例如中古英語,現代英語都是英語,而漢語不符合字詞由字母組成,所以它不是英語。同理所有服從CTS的語言,都可以被看成.NET框架的語言。
- CTS中定義了類型,允許它有屬性,字段,方法等。
- .NET框架的眾多語言各自實現了CTS的一部分功能。做一個不太恰當的類比,C#可以被認為是“美國英語”,F#是“英國英語”而VB是“印度英語”等。他們是英語的各種方言。他們共享一套相同的詞匯表,但也各有各的特點。例如顏色在英國英語中的拼寫是colour,美國英語則是color。
- 由於.NET框架的眾多語言在編譯時都要轉換為IL,因此IL實現的CTS功能是它們的並集,也就是CTS全部的功能。你可以理解為,雖然.NET框架語言那么多,但一編譯了之后,就成了一種語言。
- .NET框架的眾多語言分享CTS的一小部分功能,這部分功能稱為CLS(Common Language Specification,公共語言規范)。這是這些語言(的程序集)可以相互使用的前提。如果你創建一個新語言,其實現了CTS的一部分功能,但不包括CLS,那你的語言就不能被其他.NET框架的語言(的程序集)使用。如果你創建的語言甚至不符合CTS,例如你在詞匯表中加入了漢字,那不好意思,你創建的語言不能叫英語。
很明顯,CLS是CTS的一個子集,而且是最小的子集。(最小功能集)
圖片來自CLR via C#。
1.5 為什么說.NET是平台無關的?
.NET程序集可以在非微軟操作系統如Mac OS,各種版本的Linux,以及iOS和Android移動設備上開發和執行。.NET的平台無關性主要體現為:.NET程序集可以在任何的平台上運行,不管是Windows,還是Mac,只要這個平台擁有將IL轉換為機器碼,以及加載其他相關程序集的能力(即CLR),而任何機器都可以運行機器碼。這類似於Java的虛擬機,只要平台裝了Java虛擬機,則這個平台就可以運行Java程序。
1.6 CLR(公共語言運行時)
CLR是讓程序執行所需的外部服務的集合,類似Java需要JVM虛擬機才可以運行。
它的核心功能(比如即時編譯,內存管理,程序集加載,安全性,異常處理和線程同步)可由面向CLR的所有語言使用。例如,CLR允許創建線程,所以面向CLR的所有語言都能創建線程。
CLR是.NET的運行基礎,管理.NET程序集的執行。它運行於Windows之上,很多功能僅僅是Windows上的一個wrapper,例如線程,內存管理等,這些實際上是Windows在管理。但JIT則是它獨有的,如果沒有它,就不能把IL變成機器碼,計算機也就不認識C#,你也就不能運行C#程序。
在開始運行.NET程序之前,編譯器將代碼轉換為IL。IL代碼並不能直接運行,CLR將真正需要用到的程序集導入內存,讀取元數據,接着為類型開辟內存空間,執行所有需要的安全檢查,並最終運行代碼:
- CLR找到代碼中擁有Main方法的類型並且加載這個類型。CLR中一個名為Class loader(類加載程序)的組件負責這項工作。它會從GAC、配置文件、程序集元數據中尋找這個類型,然后將它的類型信息加載到內存中的數據結構中。在Class loader找到並加載完這個類型之后,它的類型信息會被緩存起來,這樣就無需再次進行相同的過程。當然,如果這個類型引用了其他的類型,則會導致一連串的程序集加載,這將定義程序代碼執行的環境(類似Java的JVM)。注意即使工程很大,有幾百個程序集,CLR不會全部加載,只會在真正用到該程序集的時候才加載。
- 驗證。在CLR中,還存在一個驗證程序(verifier),該驗證程序的工作是在運行時確保代碼是類型安全的。它主要校驗兩個方面,一個是元數據是正確的,一個是IL代碼必須是類型安全的,類型的簽名必須正確。這是早期綁定驗證,驗證在運行時之前發生。對於動態類型,此時不做任何檢查。
- 即時編譯。(此時就從編譯時過渡到了運行時)這一步就是將托管的IL代碼編譯為可以執行的機器代碼的過程,由CLR的即時編譯器(JIT Complier)完成。即時編譯只有在方法的第一次調用時發生。類型加載程序(Class loader)會為每個方法插入一個存根。在調用方法時,CLR會檢查方法的存根,如果存根為空,則執行JIT編譯過程,並將該方法被編譯后的本地機器代碼地址寫入到方法存根中。當第二次對同一方法進行調用時,會再次檢查這個存根,如果發現其保存了本地機器代碼的地址,則直接跳轉到本地機器代碼進行執行,無需再次進行JIT編譯。JIT編譯還會優化本地的代碼。
在程序運行時,CLR還負責:
- 異常處理
- 內存管理與垃圾回收
- 線程管理(線程池)
托管代碼是必須在CLR下執行的代碼,而非托管代碼則不需要CLR的支持就可以運行。CLR本身用於管理托管代碼,因此它是由非托管代碼編寫的,並不是一個包含了托管代碼的程序集,也不能使用IL DASM進行查看。它位於C:\%SystemRoot%\Microsoft.NET\Framework\版本號下,視安裝的機器不同有兩個版本,一個是工作站版本的mscorwks.dll,一個是服務器版本的mscorsvr.dll。wks和svr分別代表workstation和server。
CLR via C#這本書選擇通過C#作為視角,討論CLR的各種功能。通過對這本書的閱讀,你會對一些實際由CLR進行管理的行為例如垃圾回收,線程管理有更加深刻的認識。
2. 編譯:IL與JIT
面試出現頻率:低。不排除部分IL專家會試探性問你一些IL命令,但我相信你答不出來他們也不會在意。學了IL和沒學,一般人看不出來區別,學了IL,也不意味着你就很厲害。個人認為,學IL唯一的用處就在於證明你看到的書上寫的各種結論,或者驗證一些性能方面的想法。你可以參看這篇文章:http://blog.zhaojie.me/2009/06/my-view-of-il-2-il-shows-little-about-clr.html
重要程度:3/10,常識性了解即可
需要理解的程度:知道IL是中間代碼,知道JIT的優點(帶緩存的編譯),以及它可能會對你的代碼進行優化。
2.1 什么是IL(CIL)?如何獲得IL代碼?
在.NET的開發過程中, IL的官方術語是MSIL或CIL(Common Intermediate Language,即公共中間語言)。因此,IL,MSIL和CIL指的是同一種東西。
當使用支持.NET的編譯器編譯之后,生成.dll或.exe文件。這文件稱作.NET程序集,包含IL和元數據。不同語言(例如C#和VB)經過不同編譯器(例如C#編譯器和VB編譯器),編譯一段功能相似的代碼(區別僅僅在於語法),其IL也基本相似。雖然IL相對C#較為底層,但它仍然是一個十分高級的語言。它並不是匯編語言。
可以通過ildasm(在cmd中運行)工具加載任意的.NET程序集並分析它的內容,包括它所包含的IL代碼和元數據。注意,高級語言只公開了CLR的所有功能的一個子集,而IL允許開發人員訪問CLR所有的功能。
關於IL的擴展閱讀,可參看老趙談IL系列:
http://blog.zhaojie.me/2009/06/my-view-of-il-1-il-and-asm.html
2.2 什么是JIT?還有什么其他編譯方式?何時使用到JIT?
即時編譯(英語:Just-in-time compilation)是動態編譯的一種形式,是一種提高程序運行效率的方法。通常,程序有兩種運行方式:靜態編譯與動態編譯。靜態編譯的程序在執行前全部被翻譯為機器碼,而動態編譯執行的則是一句句,邊運行邊翻譯。
即時編譯則混合了這二者,一句句編譯源代碼,但是會將翻譯過的代碼緩存起來以降低性能損耗。相對於靜態編譯代碼,即時編譯的代碼可以處理延遲綁定並增強安全性。
CLR的JIT負責將IL編譯成機器碼。 當程序編譯成程序集之后,CLR加載任何需要用到的其他程序集,並開始使用JIT將CIL編譯為機器碼。JIT編譯器會在方法的首次調用時,從類型的元數據中查找方法,並進行檢查,例如檢查類型是否安全。如果出現了問題,則觸發運行時錯誤。以后對方法的所有調用都以本地代碼的形式全速運行,無須重新檢查。
2.3 本地代碼的優化
CLR的JIT編譯器會對本地代碼進行優化。例如字符串駐留中對常量字符串相加的優化。和沒有優化相比,優化之后的代碼將獲得更出色的性能。但過度的優化可能會出現問題,在CLR via C#的易失構造中,作者舉了一個例子。

1 class Program 2 { 3 private static bool s_stopWorker = false; 4 5 static void Main() 6 { 7 Console.WriteLine("Main: letting worker run for 2 seconds"); 8 Thread t = new Thread(Worker); 9 t.Start(); 10 11 Thread.Sleep(2000); 12 s_stopWorker = true; 13 Console.WriteLine("Main: waiting for worker to stop"); 14 t.Join(); 15 } 16 17 private static void Worker(object o) 18 { 19 int x = 0; 20 while (!s_stopWorker) 21 { 22 x++; 23 } 24 Console.WriteLine("Worker: stopped when x = {0}", x); 25 } 26 }
如果使用f5呼叫出Visual Studio的調試模式,則程序會像預期的那樣正常運行直到結束。使用調試器會造成JIT編譯器在Debug模式進行編譯,它生成未優化的代碼,目的是方便你進行單步調試。如果是選擇了x86的Release模式進行編譯:
它將會生成被CLR優化的代碼。值得一提的是,x86編譯器是一個更成熟的編譯器,執行優化比x64更大膽。x64不會執行上面所說的特定的優化。在再次用f6進行編譯之后,用ctrl+f5運行程序,程序將會陷入無限循環。
注意:必須用x86+Release編譯,然后以非調試模式運行(即Ctrl+F5),才能看到這個效果。問題發生的原因是,x86的編譯優化過度。它發現變量s_stopWorker要么為true要么為false。它還發現這個值在worker方法本身中從來沒有變化。因此,編譯器會生成代碼檢查s_stopWorker,如果s_stopWorker為true,就顯示“Worker: stopped when x = 0”。如果s_stopWorker為false編譯器就生成代碼進入一個無限循環,並一直遞增x。解決的辦法是為s_stopWorker加入修飾詞volatile。
PDB文件包含了可以令調試器在本地工作的信息。可以這么說:有了PDB文件,本地的debug才成為可能。如果你打算發布Release版本,則不需要該文件。使用Release模式編譯的結果中也不包含PDB文件。例如,你寫了一個小的控制台程序給別人用,那么你不需要把\bin\debug里面所有的文件都拷貝給別人,你只需要程序本身,必要的dll和config文件即可。