本文轉載自:http://www.cnblogs.com/1996V/p/9037603.html 【尊重作者原創,轉載說明出處!感謝作者“小曾看世界”分享! 】 什麼是.NET?什麼是.NET Framework?本文將從上往下,循序漸進的介紹一系列相關.NET的概念,先從類型系統開始講起,我將通 ...
本文轉載自:http://www.cnblogs.com/1996V/p/9037603.html
【尊重作者原創,轉載說明出處!感謝作者“小曾看世界”分享! 】
什麽是.NET?什麼是.NET Framework?本文將從上往下,循序漸進的介紹一系列相關.NET的概念,先從類型系統開始講起,我將通過跨語言操作這個例子來逐漸引入一系列.NET的相關概念,這主要包括:CLS、CTS(CLI)、FCL、Windows下CLR的相關核心組成、Windows下托管程式運行概念、什麼是.NET Framework,.NET Core,.NET Standard及一些VS編譯器相關雜項和相關閱讀鏈接。完整的從上讀到下則你可以理解個大概的.NET體系。
文章是我一字一字親手碼出來的,每天下班用休息時間寫一點,持續了二十來天。且對於文章上下銜接、概念引入花了很多心思,致力讓很多概念在本文中顯得通俗。但畢竟.NET系統很龐大,本文篇幅有限,所以在部分小節中我會給出延伸閱讀的鏈接,在文章結尾我給出了一些小的建議,希望能對需要幫助的人帶來幫助,如果想與我交流可以文章留言或者加.NET技術交流群:166843154
目錄
- .NET和C#是什麼關係
- 跨語言和跨平臺是什麼
- 什麼是跨語言互操作,什麼是CLS
- 什麼是CTS?
- 什麼是類庫?
- 什麼是基元類型?
- System.Object的意義
- 電腦是如何運行程式的?
- 什麼是托管代碼,托管語言,托管模塊?
- 什麼是CLR,.NET虛擬機?
- 什麼是CLR宿主進程,運行時主機?
- Windows系統自帶.NET Framework
- .NET Framework 4.0.30319
- 什麼是程式集
- 用csc.exe進行編譯
- .NET程式執行原理
- 程式集的規則
- 程式集的載入方式
- 強名稱程式集
- 程式集搜索規則
- 項目的依賴順序
- 為什麼Newtonsoft.Json版本不一致?
- 如何在編譯時載入兩個相同的程式集
- 如何同時調用兩個兩個相同命名空間和類型的程式集?
- 共用程式集GAC
- 延伸
- 應用程式域
- 記憶體
- .NET程式執行圖
- .NET的安全性
- 什麼是.NET
- Visual Studio
- 建議
.NET和C#是什麼關係
語言,是人們進行溝通表達的主要方式。編程語言,是人與機器溝通的表達方式。不同的編程語言,其側重點不同。有的編程語言是為了科學計算而開發的,所以其語法和功能更偏向於函數式思想。有些則是為了開發應用程式而創立的,所以其語法和功能更為均衡全面。
微軟公司是全球最大的電腦軟體提供商,為了占據開發者市場,進而在2002年推出了Visual Studio(簡稱VS,是微軟提供給開發者的工具集) .NET 1.0版本的開發者平臺。而為了吸引更多的開發者涌入平臺,微軟還在2002年宣佈推出一個特性強大並且與.NET平臺無縫集成的編程語言,即C# 1.0正式版。 只要是.NET支持的編程語言,開發者就可以通過.NET平臺提供的工具服務和框架支持便捷的開發應用程式。
C#就是為宣傳.NET而創立的,它直接集成於Visual Studio .NET中,VB也在.NET 1.0發佈後對其進行支持, 所以這兩門語言與.NET平臺耦合度很高,並且.NET上的技術大多都是以C#編程語言為示例,所以經常就.NET和C#混為一談(實質上它們是相輔相成的兩個概念)。 而作為一個開發者平臺,它不僅僅是包含開發環境、技術框架、社區論壇、服務支持等,它還強調了平臺的跨語言、跨平臺編程的兩個特性。
跨語言和跨平臺是什麼
跨語言:即只要是面向.NET平臺的編程語言((C#、Visual Basic、C++/CLI、Eiffel、F#、IronPython、IronRuby、PowerBuilder、Visual COBOL 以及 Windows PowerShell)),用其中一種語言編寫的類型可以無縫地用在另一種語言編寫的應用程式中的互操作性。 跨平臺:一次編譯,不需要任何代碼修改,應用程式就可以運行在任意有.NET框架實現的平臺上,即代碼不依賴於操作系統,也不依賴硬體環境。
什麼是跨語言互操作,什麼是CLS
每門語言在最初被設計時都有其在功能和語法上的定位,讓不同的人使用擅長的語言去乾合適的事,這在團隊協作時尤為重要。 .NET平臺上的跨語言是通過CLS這個概念來實現的,接下來我就以C#和VB來演示 什麼是.NET中的跨語言互操作性。
通俗來說,雖然c#和vb是兩個不同的語言,但此處c#寫的類可以在vb中當做自家寫的類一樣正常使用。
比如我在vb中寫了一個針對String的首字母大寫的擴展方法,將其編譯後的dll引用至C#項目中。
在C#項目中,可以像自身代碼一樣正常使用來自vb這個dll的擴展方法。
現在有那麼多面向對象語言,但不是所有編程語言都能這樣直接互操作使用,而.NET平臺支持的C#和VB之所以能這樣無縫銜接,先讀而後知,後文將會介紹緣由。不過雖然.NET平臺提供了這樣一個互操作的特性,但終究語言是不一樣的,每個語言有其特色和差異處,在相互操作的時候就會難免遇到一些例外情況。
比如我在C#中定義了一個基類,類裡面包含一個公開的指針類型的成員,我想在vb中繼承這個類,並訪問這個公開的成員。
但是vb語言因為其定位不需要指針,所以並沒有C#中如int*這樣的指針類型,所以在vb中訪問一個該語言不支持的類型會報錯的,會提示:欄位的類型不受支持。
再比如,C#語言中,對類名是區分大小寫的,我在C#中定義了兩個類,一個叫BaseBusiness,另一個叫baseBusiness。我在vb中去繼承這個BaseBusiness類。
如圖,在vb中訪問這個類會報錯的,報:"BaseBusiness"不明確,這是因為在vb中對類名是不區分大小寫的。在vb中,它認為它同時訪問了兩個一模一樣的類,所以按照vb的規則這是不合理的。那麼為了在vb調用c#的程式集中避免這些因語言的差異性而導致的錯誤,在編寫c#代碼的時候 就應該提前知道vb中的這些規則,來應付式的開發。
但是,如果我想不僅僅局限於C#和VB,我還想我編寫的代碼在.Net平臺上通用的話,那麼我還必須得知道.NET平臺支持的每一種語言和我編寫代碼所使用的語言的差異,從而在編寫代碼中避免這些。
這幾年編程語言層出不窮,在將來.NET可能還會支持更多的語言,如果說對一個開發者而言掌握所有語言的差異處這是不現實的,所以.NET專門為此參考每種語言並找出了語言間的共性,然後定義了一組規則,開發者都遵守這個規則來編碼,那麼代碼就能被任意.NET平臺支持的語言所通用。 而與其說是規則,不如說它是一組語言互操作的標準規範,它就是公共語言規範 - Common Language Specification ,簡稱CLS
CLS從類型、命名、事件、屬性、數組等方面對語言進行了共性的定義及規範。這些東西被提交給歐洲電腦製造聯合會ECMA,稱為:共同語言基礎設施。
就以類型而言,CLS定義了在C#語言中符合規範的類型和不符合的有:
當然,就編碼角度而言,我們不是必須要看那些詳略的文檔。為了方便開發者開發,.NET提供了一個特性,名叫:CLSCompliantAttribute,代碼被CLSCompliantAttribute標記後,如果你寫的代碼不符合CLS規範的話,編譯器就會給你一條警告。
值得一提的是,CLS規則只是面向那些公開可被其它程式集訪問的成員,如public、繼承的protected,對於該程式集的內部成員如Private、internal則不會執行該檢測規則。也就是說,所適應的CLS遵從性規則,僅是那些公開的成員,而非私有實現。
那麼有沒有那種特殊情況,比如我通過反射技術來訪問該程式集中,當前語言並不擁有的類型時會發生什麼情況呢?
答案是可以嘗試的,如用vb反射訪問c#中的char*指針類型,即使vb中沒有char*這種等價的指針類型,但mscorlib提供了針對指針類型的 Pointer 包裝類供其訪問,可以從運行時類攜帶的類型名稱看到其原本的類型名。
可以看到,該類中的元素是不符合CLS規範的。
CLS異常
提到特殊情況,還要說的一點就是異常處理。.NET框架組成中定義了異常類型系統,在編譯器角度,所有catch捕獲的異常都必須繼承自System.Exception,如果你要調用一個 由不遵循此規範的語言 拋出其它類型的異常對象(C++允許拋出任何類型的異常,如C#調用C++代碼,C++拋出一個string類型的異常),在C#2.0之前Catch(Exception)是捕捉不了的,但之後的版本可以。 在後續版本中,微軟提供了System.Runtime.CompilerServices.RuntimeWrappedException異常類,將那些不符合CLS的包含Exception的對象封裝起來。並且可以通過RuntimeCompatibilityAttribute特性來過濾這些異常。 RuntimeWrappedException :https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.compilerservices.runtimewrappedexception?view=netframework-4.7.2
那麼,這個段落總結一下,什麼是CLS呢?
在面向.NET開發中,編寫跨語言組件時所遵循的那些共性,那些規範就叫做 Common Langrage Specification簡稱 CLS,公共語言規範 官方CLS介紹:https://docs.microsoft.com/zh-cn/dotnet/standard/language-independence-and-language-independent-components
什麼是CTS?
如果理解了什麼是CLS的話,那麼你將很輕鬆理解什麼是CTS。 假設你已經圍繞著封裝 繼承 多態 這3個特性設計出了多款面向對象的語言,你發現大家都是面向對象,都能很好的將現實中的對象模型表達出來。除了語法和功能擅長不同,語言的定義和設計結構其實都差不多一回事。
比如,現實中你看到了一輛小汽車,這輛車裡坐著兩個人,那麼如何用這門語言來表達這樣的一個概念和場面? 首先要為這門語言橫向定義一個“類型”的概念。接下來在程式中就可以這樣表示:有一個汽車類型,有一個人類型,在一個汽車類型的對象內包含著兩個人類型的對象,因為要表達出這個模型,你又引入了“對象”的概念 。而現在,你又看到,汽車裡面的人做出了開車的這樣一個動作,由此你又引入了“動作指令”這樣一個概念。 接著,你又恍然大悟總結出一個定理,無論是什麼樣的“類型”,都只會存在這樣一個特征,即活著的 帶生命特征的(如人) 和 死的 沒有生命特征的(如汽車) 這兩者中的一個。最後,隨著思想模型的成熟,你發現,這個“類型”就相當於一個富有主體特征的一組指令的集合。 好,然後你開始照葫蘆畫瓢。你參考其它程式語言,你發現大家都是用class來表示類的含義,用struct表示結構的含義,用new來表示 新建一個對象的含義,於是,你對這部分功能的語法也使用class和new關鍵字來表示。然後你又發現,他們還用很多關鍵字來更豐富的表示這些現實模型,比如override、virtual等。於是,在不斷的思想升級和借鑒後,你對這個設計語言過程中思想的變化仔細分析,對這套語言體系給抽象歸納,最終總結出一套體系。
於是你對其它人這樣說,我總結出了一門語言很多必要的東西如兩種主要類別:值類別和引用類別,五個主要類型:類、介面、委托、結構、枚舉,我還規定了,一個類型可以包含欄位、屬性、方法、事件等成員,我還指定了每種類型的可見性規則和類型成員的訪問規則,等等等等,只要按照我這個體系來設計語言,設計出來的語言它能夠擁有很多不錯的特性,比如跨語言,跨平臺等,C#和VB.net之所以能夠這樣就是因為這兩門語言的設計符合我這個體系。
那麼,什麼是CTS呢?
當你需要設計面向.Net的語言時所需要遵循一個體系(.Net平臺下的語言都支持的一個體系)這個體系就是CTS(Common Type System 公共類型系統),它包括但不限於:
- 建立用於跨語言執行的框架。
- 提供面向對象的模型,支持在 .NET 實現上實現各種語言。
- 定義處理類型時所有語言都必須遵守的一組規則(CLS)。
- 提供包含應用程式開發中使用的基本基元數據類型(如 Boolean、Byte、Char 等)的庫。
上文的CLS是CTS(Common Type System 公共類型系統)這個體系中的子集。 一個編程語言,如果它能夠支持CTS,那麼我們就稱它為面向.NET平臺的語言。 官方CTS介紹: https://docs.microsoft.com/zh-cn/dotnet/standard/common-type-system
微軟已經將CTS和.NET的一些其它組件,提交給ECMA以成為公開的標準,最後形成的標準稱為CLI(Common Language Infrastructure)公共語言基礎結構。 所以有的時候你見到的書籍或文章有的只提起CTS,有的只提起CLI,請不要奇怪,你可以寬泛的把他們理解成一個意思,CLI是微軟將CTS等內容提交給國際組織電腦製造聯合會ECMA的一個工業標準。
什麼是類庫?
在CTS中有一條就是要求基元數據類型的類庫。我們先搞清什麼是類庫?類庫就是類的邏輯集合,你開發工作中你用過或自己編寫過很多工具類,比如搞Web的經常要用到的 JsonHelper、XmlHelper、HttpHelper等等,這些類通常都會在命名為Tool、Utility等這樣的項目中。 像這些類的集合我們可以在邏輯上稱之為 "類庫",比如這些Helper我們統稱為工具類庫。
什麼是基礎類庫BCL?
當你通過VS創建一個項目後,你這個項目就已經引用好了通過.NET下的語言編寫好的一些類庫。比如控制臺中你直接就可以用ConSole類來輸出信息,或者using System.IO 即可通過File類對文件進行讀取或寫入操作,這些類都是微軟幫你寫好的,不用你自己去編寫,它幫你編寫了一個面向.NET的開發語言中使用的基本的功能,這部分類,我們稱之為BCL(Base Class Library), 基礎類庫,它們大多都包含在System命名空間下。
基礎類庫BCL包含:基本數據類型,文件操作,集合,自定義屬性,格式設置,安全屬性,I/O流,字元串操作,事件日誌等的類型
什麼是框架類庫FCL?
有關BCL的就不在此一一類舉。.NET之大,發展至今,由微軟幫助開發人員編寫的類庫越來越多,這讓我們開發人員開發更加容易。由微軟開發的類庫統稱為:FCL,Framework Class Library ,.NET框架類庫,我上述所表達的BCL就是FCL中的一個基礎部分,FCL中大部分類都是通過C#來編寫的。
在FCL中,除了最基礎的那部分BCL之外,還包含我們常見的 如 : 用於網站開發技術的 ASP.NET類庫,該子類包含webform/webpage/mvc,用於桌面開發的 WPF類庫、WinForm類庫,用於通信交互的WCF、asp.net web api、Web Service類庫等等
什麼是基元類型?
像上文在CTS中提到了 基本基元數據類型,大家知道,每門語言都會定義一些基礎的類型,比如C#通過 int 來定義整型,用 string 來定義 字元串 ,用 object 來定義 根類。當我們來描述這樣一個類型的對象時可以有這兩種寫法,如圖:
我們可以看到,上邊用首字母小寫的藍色體string、object能描述,用首字母大寫的淺藍色String、Object也能描述,這兩種表述方式有何不同?
要知道,在vs預設的顏色方案中,藍色體 代表關鍵字,淺藍色體 代表類型。 那麼這樣也就意味著,由微軟提供的FCL類庫裡面 包含了 一些用於描述數據類型的 基礎類型,無論我們使用的是什麼語言,只要引用了FCL,我們都可以通過new一個類的方式來表達數據類型。 如圖:
用new來創建這些類型的對象,但這樣就太繁瑣,所以C#就用 int關鍵字來表示System.Int32,用 string關鍵字來表示 System.String等,所以我們才能這樣去寫。
像這樣被表述於編譯器直接支持的類型叫做基元類型,它被直接映射於BCL中具體的類。
下麵是部分面向.NET的語言的基元類型與對應的BCL的類別圖 :
System.Object的意義
說起類型,這裡要說CTS定義的一個非常重要的規則,就是類與類之間只能單繼承,System.Object類是所有類型的根,任何類都是顯式或隱式的繼承於System.Object。
System.Object定義了類型的最基本的行為:用於實例比較的Equals系列方法、用於Hash表中Hash碼的GetHashCode、用於Clr運行時獲取的類型信息GetType、用於表示當前對象字元串的ToString、用於執行實例的淺複製MemberwiseClone、用於GC回收前操作的析構方法Finalize 這6類方法。
所以 Object不僅是C#語言的類型根、還是VB等所有面向.NET的語言的類型根,它是整個FCL的類型根。
當然,CTS定義了單繼承,很多編程語言都滿足這個規則,但也有語言是例外,如C++就不做繼承限制,可以繼承多個,C++/CLI作為C++在對.NET的CLI實現,如果在非托管編碼中多繼承那也可以,如果試圖在托管代碼中多繼承,那就會報錯。我前面已經舉過這樣特殊情況的例子,這也在另一方面反映出,各語言對CTS的支持並不是都如C#那樣全面的,我們只需明記一點:對於符合CTS的那部分自然就按照CTS定義的規則來。 任何可遵循CTS的類型規範,同時又有.NET運行時的實現的編程語言就可以成為.NET中的一員。
電腦是如何運行程式的?
接下來我要說什麼是.NET的跨平臺,並解釋為什麼能夠跨語言。不過要想知道什麼是跨平臺,首先你得知道一個程式是如何在本機上運行的。
什麼是CPU
CPU,全稱Central Processing Unit,叫做中央處理器,它是一塊超大規模的集成電路,是電腦組成上必不可少的組成硬體,沒了它,電腦就是個殼。 無論你編程水平怎樣,你都應該先知道,CPU是一臺電腦的運算核心和控制核心,CPU從存儲器或高速緩衝存儲器中取出指令,放入指令寄存器,並對指令解碼,執行指令。 我們運行一個程式,CPU就會不斷的讀取程式中的指令並執行,直到關閉程式。事實上,從電腦開機開始,CPU就一直在不斷的執行指令直到電腦關機。
什麼是高級編程語言
在電腦角度,每一種CPU類型都有自己可以識別的一套指令集,電腦不管你這個程式是用什麼語言來編寫的,其最終只認其CPU能夠識別的二進位指令集。 在早期電腦剛發展的時代,人們都是直接輸入01010101這樣的沒有語義的二進位指令來讓電腦工作的,可讀性幾乎沒有,沒人願意直接編寫那些沒有可讀性、繁瑣、費時,易出差錯的二進位01代碼,所以後來才出現了編程語言。
編程語言的誕生,使得人們編寫的代碼有了可讀性,有了語義,與直接用01相比,更有利於記憶。 而前面說了,電腦最終只識別二進位的指令,那麼,我們用編程語言編寫出來的代碼就必須要轉換成供機器識別的指令。 就像這樣:
code: 1+2 function 翻譯方法(參數:code) { ... "1"=>"001"; "2"=>"002"; "+"=>"000"; return 能讓機器識別的二進位代碼; } call 翻譯方法("1+2") => "001 000 002"
所以從一門編程語言所編寫的代碼文件轉換成能讓本機識別的指令,這中間是需要一個翻譯的過程。 而我們現在電腦上是運載著操作系統的,光翻譯成機器指令也不行,還得讓代碼文件轉化成可供操作系統執行的程式才行。 那麼這些步驟,就是編程語言所對應的編譯環節的工程了。這個翻譯過程是需要工具來完成,我們把它叫做 編譯器。
不同廠商的CPU有著不同的指令集,為了剋服面向CPU的指令集的難讀、難編、難記和易出錯的缺點,後來就出現了面向特定CPU的特定彙編語言, 比如我打上這樣的x86彙編指令 mov ax,bx ,然後用上用機器碼做的彙編器,它將會被翻譯成 1000100111011000 這樣的二進位01格式的機器指令.
不同CPU架構上的彙編語言指令不同,而為了統一一套寫法,同時又不失彙編的表達能力,C語言就誕生了。 用C語言寫的代碼文件,會被C編譯器先轉換成對應平臺的彙編指令,再轉成機器碼,最後將這些過程中產生的中間模塊鏈接成一個可以被操作系統執行的程式。
那麼彙編語言和C語言比較,我們就不需要去閱讀特定CPU的彙編碼,我只需要寫通用的C源碼就可以實現程式的編寫,我們用將更偏機器實現的彙編語言稱為低級語言,與彙編相比,C語言就稱之為高級語言。
在看看我們C#,我們在編碼的時候都不需要過於偏向特定平臺的實現,翻譯過程也基本遵循這個過程。它的編譯模型和C語言類似,都是屬於這種間接轉換的中間步驟,故而能夠跨平臺。 所以就類似於C/C#等這樣的高級語言來說是不區分平臺的,而在於其背後支持的這個 翻譯原理 是否能支持其它平臺。
什麼是托管代碼,托管語言,托管模塊?
作為一門年輕的語言,C#借鑒了許多語言的長處,與C比較,C#則更為高級。 往往一段簡小的C#代碼,其功能卻相當於C的一大段代碼,並且用C#語言你幾乎不需要指針的使用,這也就意味著你幾乎不需要進行人為的記憶體管控與安全考慮因素,也不需要多懂一些操作系統的知識,這讓編寫程式變得更加輕鬆和快捷。
如果說C#一段代碼可以完成其它低級語言一大段任務,那麼我們可以說它特性豐富或者類庫豐富。而用C#編程不需要人為記憶體管控是怎麼做到的呢? .NET提供了一個垃圾回收器(GC)來完成這部分工作,當你創建類型的時候,它會自動給你分配所需要的這部分記憶體空間。就相當於,有一個專門的軟體或進程,它會讀取你的代碼,然後當你執行這行代碼的時候,它幫你做了記憶體分配工作。 這部分本該你做的工作,它幫你做了,這就是“托管”的概念。比如現實中 托管店鋪、托管教育等這樣的別人替你完成的概念。
因此,C#被稱之為托管語言。C#編寫的代碼也就稱之為托管代碼,C#生成的模塊稱之為托管模塊等。(對於托管的資源,是不需要也無法我們人工去干預的,但我們可以瞭解它的一些機制原理,在後文我會簡單介紹。)
只要有比較,就會產生概念。那麼在C#角度,那些脫離了.NET提供的諸如垃圾回收器這樣的環境管制,就是對應的 非托管了。
非托管的異常
我們編寫的程式有的模塊是由托管代碼編寫,有的模塊則調用了非托管代碼。在.NET Framework中也有一套基於此操作系統SEH的異常機制,理想的機制設定下我們可以直接通過catch(e)或catch來捕獲指定的異常和框架設計人員允許我們捕獲的異常。
而異常類型的級別也有大有小,有小到可以直接框架本身或用代碼處理的,有大到需要操作系統的異常機制來處理。.NET會對那些能讓程式崩潰的異常類型給進行標記,對於這部分異常,在.NET Framework 4.0之前允許開發人員在代碼中自己去處理,但4.0版本之後有所變更,這些被標記的異常預設不會在托管環境中拋出(即無法catch到),而是由操作系統的SEH機制去處理。 不過如果你仍然想在代碼中捕獲處理這樣的異常也是可以的,你可以對需要捕獲的方法上標記[System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptionsAttribute]特性,就可以在該方法內通過catch捕獲到該類型的異常。你也可以通過在配置文件中添加運行時節點來對全局進行這樣的一個配置:
HandleProcessCorruptedStateExceptions特性:https://msdn.microsoft.com/zh-cn/library/azure/system.runtime.exceptionservices.handleprocesscorruptedstateexceptionsattribute.aspx SEHException類:https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.sehexception(v=vs.100).aspx 處理損壞狀態異常博客專欄: https://msdn.microsoft.com/zh-cn/magazine/dd419661.aspx
什麼是CLR,.NET虛擬機?
實際上,.NET不僅提供了自動記憶體管理的支持,他還提供了一些列的如類型安全、應用程式域、異常機制等支持,這些 都被統稱為CLR公共語言運行庫。
CLR是.NET類型系統的基礎,所有的.NET技術都是建立在此之上,熟悉它可以幫助我們更好的理解框架組件的核心、原理。 在我們執行托管代碼之前,總會先運行這些運行庫代碼,通過運行庫的代碼調用,從而構成了一個用來支持托管程式的運行環境,進而完成諸如不需要開發人員手動管理記憶體,一套代碼即可在各大平臺跑的這樣的操作。
這套環境及體系之完善,以至於就像一個小型的系統一樣,所以通常形象的稱CLR為".NET虛擬機"。那麼,如果以進程為最低端,進程的上面就是.NET虛擬機(CLR),而虛擬機的上面才是我們的托管代碼。換句話說,托管程式實際上是寄宿於.NET虛擬機中。
什麼是CLR宿主進程,運行時主機?
那麼相對應的,容納.NET虛擬機的進程就是CLR宿主進程了,該程式稱之為運行時主機。
這些運行庫的代碼,全是由C/C++編寫,具體表現為以mscoree.dll為代表的核心dll文件,該dll提供了N多函數用來構建一個CLR環境 ,最後當運行時環境構建完畢(一些函數執行完畢)後,調用_CorDllMain或_CorExeMain來查找並執行托管程式的入口方法(如控制台就是Main方法)。
如果你足夠熟悉CLR,那麼你完全可以在一個非托管程式中通過調用運行庫函數來定製CLR並執行托管代碼。 像SqlServer就集成了CLR,可以使用任何 .NET Framework 語言編寫存儲過程、觸發器、用戶定義類型、用戶定義函數(標量函數和表值函數)以及用戶定義的聚合函數。
有關CLR大綱介紹: https://msdn.microsoft.com/zh-cn/library/9x0wh2z3(v=vs.85).aspx CLR集成: https://docs.microsoft.com/zh-cn/previous-versions/sql/sql-server-2008/ms131052(v%3dsql.100) 構造CLR的介面:https://msdn.microsoft.com/zh-cn/library/ms231039(v=vs.85).aspx 適用於 .NET Framework 2.0 的宿主介面:https://msdn.microsoft.com/zh-cn/library/ms164336(v=vs.85).aspx 選擇CLR版本: https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/startup/supportedruntime-element
所以C#編寫的程式如果想運行就必須要依靠.NET提供的CLR環境來支持。 而CLR是.NET技術框架中的一部分,故只要在Windows系統中安裝.NET Framework即可。
Windows系統自帶.NET Framework
Windows系統預設安裝的有.NET Framework,並且可以安裝多個.NET Framework版本,你也不需要因此卸載,因為你使用的應用程式可能依賴於特定版本,如果你移除該版本,則應用程式可能會中斷。
Microsoft .NET Framework百度百科下有windows系統預設安裝的.NET版本
圖出自 https://baike.baidu.com/item/Microsoft%20.NET%20Framework/9926417?fr=aladdin
.NET Framework 4.0.30319
在%SystemRoot%\Microsoft.NET下的Framework和Framework64文件夾中分別可以看到32位和64位的.NET Framework安裝的版本。 我們點進去可以看到以.NET版本號為命名的文件夾,有2.0,3.0,3.5,4.0這幾個文件夾。
.NET Framework4.X覆蓋更新
要知道.NET Framework版本目前已經迭代到4.7系列,電腦上明明安裝了比4.0更高版本的.NET Framework,然而從文件夾上來看,最高不過4.0,這是為何? 原來自.NET Framework 4以來的所有.NET Framework版本都是直接在v4.0.30319文件夾上覆蓋更新,並且無法安裝以前的4.x系列的老版本,所以v4.0.30319這個目錄中其實放的是你最後一次更新的NET Framework版本。 .NET Framework覆蓋更新:https://docs.microsoft.com/en-us/dotnet/framework/install/guide-for-developers
如何確認本機安裝了哪些.NET Framework和對應CLR的版本?
我們可以通過註冊表等其它方式來查看安裝的最新版本:https://docs.microsoft.com/zh-cn/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed 。 不過如果不想那麼複雜的話,還有種最直接簡單的: 那就是進入該目錄文件夾,隨便找到幾個文件對其右鍵,然後點擊詳細信息即可查看到對應的文件版本,可以依據文件版本估摸出.NET Framework版本,比如csc.exe文件。
什麼是程式集
上文我介紹了編譯器,即將源代碼文件給翻譯成一個電腦可識別的二進位程式。而在.NET Framework目錄文件夾中就附帶的有 用於C#語言的命令行形式的編譯器csc.exe 和 用於VB語言的命令行形式的編譯器vbc.exe。
我們通過編譯器可以將尾碼為.cs(C#)和.vb(VB)類型的文件編譯成程式集。 程式集是一個抽象的概念,不同的編譯選項會產生不同形式的程式集。以文件個數來區分的話,那麼就分 單文件程式集(即一個文件)和多文件程式集(多個文件)。 而不論是單文件程式集還是多文件程式集,其總有一個核心文件,就是表現為尾碼為.dll或.exe格式的文件。它們都是標準的PE格式的文件,主要由4部分構成:
- 1.PE頭,即Windows系統上的可移植可執行文件的標準格式
- 2.CLR頭,它是托管模塊特有的,它主要包括
- 1)程式入口方法
- 2)CLR版本號等一些標誌
- 3)一個可選的強名稱數字簽名
- 4)元數據表,主要用來記錄了在源代碼中定義和引用的所有的類型成員(如方法、欄位、屬性、參數、事件...)的位置和其標誌Flag(各種修飾符) 正是因為元數據表的存在,VS才能智能提示,反射才能獲取MemberInfo,CLR掃描元數據表即可獲得該程式集的相關重要信息,所以元數據表使得程式集擁有了自我描述的這一特性。clr2中,元數據表大概40多個,其核心按照用途分為3類:
- 1.即用於記錄在源代碼中所定義的類型的定義表:ModuleDef、TypeDef、MethodDef、ParamDef、FieldDef、PropertyDef、EventDef,
- 2.引用了其它程式集中的類型成員的引用表:MemberRef、AssemblyRef、ModuleRef、TypeRef
- 3. 用於描述一些雜項(如版本、發佈者、語言文化、多文件程式集中的一些資源文件等)的清單表:AssemblyDef、FileDef、ManifestResourceDef、ExportedTypeDef
- 3.IL代碼(也稱MSIL,後來被改名為CIL:Common Intermediate Language通用中間語言),是介於源代碼和本機機器指令中間的代碼,將通過CLR在不同的平臺產生不同的二進位機器碼。
- 4.一些資源文件
多文件程式集的誕生場景有:比如我想為.exe綁定資源文件(如Icon圖標),或者我想按照功能以增量的方式來按需編譯成.dll文件。 通常很少情況下才會將源代碼編譯成多文件程式集,並且在VS IDE中總是將源代碼給編譯成單文件的程式集(要麼是.dll或.exe),所以接下來我就以單文件程式集為例來講解。
用csc.exe進行編譯
現在,我將演示一段文本是如何被csc.exe編譯成一個可執行的控制台程式的。 我們新建個記事本,然後將下麵代碼複製上去。
1 using System; 2 using System.IO; 3 using System.Net.Sockets; 4 using System.Text; 5 class Program 6 { 7 static void Main() 8 { 9 string rootDirectory = Environment.CurrentDirectory; 10 Console.WriteLine("開始連接,埠號:8090"); 11 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 12 socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 8090)); 13 socket.Listen(30); 14 while (true) 15 { 16 Socket socketClient = socket.Accept(); 17 Console.WriteLine("新請求"); 18 byte[] buffer = new byte[4096]; 19 int length = socketClient.Receive(buffer, 4096, SocketFlags.None); 20 string requestStr = Encoding.UTF8.GetString(buffer, 0, length); 21 Console.WriteLine(requestStr); 22 // 23 string[] strs = requestStr.Split(new string[] { "\r\n" }, StringSplitOptions.None); 24 string url = strs[0].Split(' ')[1]; 25 26 byte[] statusBytes, headerBytes, bodyBytes; 27 28 if (Path.GetExtension(url) == ".jpg") 29 { 30 string status = "HTTP/1.1 200 OK\r\n"; 31 statusBytes = Encoding.UTF8.GetBytes(status); 32 bodyBytes = File.ReadAllBytes(rootDirectory + url); 33 string header = string.Format("Content-Type:image/jpg;\r\ncharset=UTF-8\r\nContent-Length:{0}\r\n", bodyBytes.Length); 34 headerBytes = Encoding.UTF8.GetBytes(header); 35 } 36 else 37 { 38 if (url == "/") 39 url = "預設頁"; 40 string status = "HTTP/1.1 200 OK\r\n"; 41 statusBytes = Encoding.UTF8.GetBytes(status); 42 string body = "<html>" + 43 "<head>" + 44 "<title>socket webServer -- Login</title>" + 45 "</head>" + 46 "<body>" + 47 "<div style=\"text-align:center\">" + 48 "當前訪問" + url + 49 "</div>" + 50 "</body>" + 51 "</html>"; 52 bodyBytes = Encoding.UTF8.GetBytes(body); 53 string header = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n", bodyBytes.Length); 54 headerBytes = Encoding.UTF8.GetBytes(header); 55 } 56 socketClient.Send(statusBytes); 57 socketClient.Send(headerBytes); 58 socketClient.Send(new byte[] { (byte)'\r', (byte)'\n' }); 59 socketClient.Send(bodyBytes); 60 61 socketClient.Close(); 62 } 63 } 64 }View Code
然後關閉記事本,將之.txt的尾碼改為.cs的尾碼(尾碼是用來標示這個文件是什麼類型的文件,並不影響文件的內容)。
上述代碼相當於Web中的http.sys偽實現,是建立了通信的socket服務端,並通過while迴圈來不斷的監視獲取包的數據實現最基本的監聽功能,最終我們將通過csc.exe將該文本文件編譯成一個控制台程式。
我已經在前面講過BCL,基礎類庫。在這部分代碼中,為了完成我想要的功能,我用到了微軟已經幫我們實現好了的String數據類型系列類(.NET下的一些數據類型)、Environment類(提供有關當前環境和平臺的信息以及操作它們的方法)、Console類(用於控制台輸入輸出等)、Socket系列類(對tcp協議抽象的介面)、File文件系列類(對文件目錄等操作系統資源的一些操作)、Encoding類(字元流的編碼)等 這些類,都屬於BCL中的一部分,它們存在但不限於mscorlib.dll、System.dll、System.core.dll、System.Data.dll等這些程式集中。 附:不要糾結BCL到底存在於哪些dll中,總之,它是個物理分散,邏輯上的類庫總稱。
mscorlib.dll和System.dll的區別:https://stackoverflow.com/questions/402582/mscorlib-dll-system-dll
因為我用了這些類,那麼按照編程規則我必須在代碼中using這些類的命名空間,並通過csc.exe中的 /r:dll路徑 命令來為生成的程式集註冊元數據表(即以AssemblyRef為代表的程式集引用表)。 而這些代碼引用了4個命名空間,但實際上它們只被包含在mscorlib.dll和System.dll中,那麼我只需要在編譯的時候註冊這兩個dll的信息就行了。
好,接下來我將通過cmd運行csc.exe編譯器,再輸入編譯命令: csc /out:D:\demo.exe D:\dic\demo.cs /r:D:\dic\System.dll
/r:是將引用dll中的類型數據註冊到程式集中的元數據表中 。 /out:是輸出文件的意思,如果沒有該命令則預設輸出{name}.exe。 使用csc.exe編譯生成: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/compiler-options/command-line-building-with-csc-exe csc編譯命令行介紹:https://www.cnblogs.com/shuang121/archive/2012/12/24/2830874.html
總之,你除了要掌握基本的編譯指令外,當你打上這行命令並按回車後,必須滿足幾個條件,1.是.cs尾碼的c#格式文件,2.是 代碼語法等檢測分析必須正確,3.是 使用的類庫必須有出處(引用的dll),當然 因為我是編譯為控制台程式,所以還必須得有個靜態Main方法入口,以上缺一不可。
可以看出,這段命令我是將 位於D:\dic\的demo.cs文件給編譯成 位於D:\名為demo.exe的控制台文件,並且因為在代碼中使用到了System.dll,所以還需要通過/r註冊該元數據表。 這裡得註意為什麼沒有/r:mscorlib.dll,因為mscorlib.dll地位的特殊,所以csc總是對每個程式集進行mscorlib.dll的註冊(自包含引用該dll),因此我們可以不用/r:mscorlib.dll這個引用命令,但為了演示效果我還是決定通過/nostdlib命令來禁止csc預設導入mscorlib.dll文件。
所以,最終命令是這樣的: csc D:\dic\demo.cs /r:D:\dic\mscorlib.dll /r:D:\dic\System.dll /nostdlib
因為沒有指定輸出文件/out選項, 所以會預設輸出在與csc同一目錄下名為demo.exe的文件。事實上,在csc的命令中,如果你沒有指定路徑,那麼就預設採用在csc.exe的所在目錄的相對路徑。
而我們可以看到,在該目錄下有許多程式集,其中就包含我們需要的System.dll和mscorlib.dll,所以我們完全可以直接/r:mscorlib.dll /r:System.dll
而類似於System.dll、System.Data.dll這樣使用非常頻繁的程式集,我們其實不用每次編譯的時候都去手動/r一下,對於需要重覆勞動的編譯指令,我們可以將其放在尾碼為.rsp的指令文件中,然後在編譯時直接調用文件即可執行裡面的命令 @ {name}.rsp。
csc.exe預設包含csc.rsp文件,我們可以用/noconfig來禁止預設包含,而csc.rsp裡面已經寫好了我們會經常用到的指令。 所以,最終我可以這樣寫 csc D:\dic\demo.cs 直接生成控制台應用程式。
.NET程式執行原理
好的,現在我們已經有了一個demo.exe的可執行程式,它是如何被我們運行的?。
C#源碼被編譯成程式集,程式集內主要是由一些元數據表和IL代碼構成,我們雙擊執行該exe,Windows載入器將該exe(PE格式文件)給映射到虛擬記憶體中,程式集的相關信息都會被載入至記憶體中,並查看PE文件的入口點(EntryPoint)並跳轉至指定的mscoree.dll中的_CorExeMain函數,該函數會執行一系列相關dll來構造CLR環境,當CLR預熱後調用該程式集的入口方法Main(),接下來由CLR來執行托管代碼(IL代碼)。
JIT編譯
前面說了,電腦最終只識別二進位的機器碼,在CLR下有一個用來將IL代碼轉換成機器碼的引擎,稱為Just In Time Compiler,簡稱JIT,CLR總是先將IL代碼按需通過該引擎編譯成機器指令再讓CPU執行,在這期間CLR會驗證代碼和元數據是否類型安全(在對象上只調用正確定義的操作、標識與聲稱的要求一致、對類型的引用嚴格符合所引用的類型),被編譯過的代碼無需JIT再次編譯,而被編譯好的機器指令是被存在記憶體當中,當程式關閉後再打開仍要重新JIT編譯。
AOT編譯
CLR的內嵌編譯器是即時性的,這樣的一個很明顯的好處就是可以根據當時本機情況生成更有利於本機的優化代碼,但同樣的,每次在對代碼編譯時都需要一個預熱的操作,它需要一個運行時環境來支持,這之間還是有消耗的。
而與即時編譯所對應的,就是提前編譯了,英文為Ahead of Time Compilation,簡稱AOT,也稱之為靜態編譯。 在.NET中,使用Ngen.exe或者開源的.NET Native可以提前將代碼編譯成本機指令。
Ngen是將IL代碼提前給全部編譯成本機代碼並安裝在本機的本機映像緩存中,故而可以減少程式因JIT預熱的時間,但同樣的也會有很多註意事項,比如因JIT的喪失而帶來的一些特性就沒有了,如類型驗證。Ngen僅是儘可能代碼提前編譯,程式的運行仍需要完整的CLR來支持。
.NET Native在將IL轉換為本機代碼的時候,會嘗試消除所有元數據將依靠反射和元數據的代碼替換為靜態本機代碼,並且將完整的CLR替換為主要包含垃圾回收器的重構運行時mrt100_app.dll。
.NET Native: https://docs.microsoft.com/zh-cn/dotnet/framework/net-native/ Ngen.exe:https://docs.microsoft.com/zh-cn/dotnet/framework/tools/ngen-exe-native-image-generator Ngen與.NET Native比較:https://www.zhihu.com/question/27997478/answer/38978762
---------------------------------------------------
現在,我們可以通過ILDASM工具(一款查看程式集IL代碼的軟體,在Microsoft SDKs目錄中的子目錄中)來查看該程式集的元數據表和Main方法中間碼。
c#源碼第一行代碼:string rootDirectory = Environment.CurrentDirectory;被翻譯成IL代碼: call string [mscorlib/*23000001*/]System.Environment/*01000004*/::get_CurrentDirectory() /* 0A000003 */
這句話意思是調用 System.Environment類的get_CurrentDirectory()方法(屬性會被編譯為一個私有欄位+對應get/set方法)。
點擊視圖=>元信息=>顯示,即可查看該程式集的元數據。 我們可以看到System.Environment標記值為01000004,在TypeRef類型引用表中找到該項: