1. 程式人生 > >什麼是.NET?什麼是.NET Framework?什麼是.NET Core?

什麼是.NET?什麼是.NET Framework?什麼是.NET Core?

什麼是.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#是什麼關係

語言,是人們進行溝通表達的主要方式。程式語言,是人與機器溝通的表達方式。不同的程式語言,其側重點不同。有的程式語言是為了科學計算而開發的,所以其語法和功能更偏向於函式式思想。有些則是為了開發應用程式而創立的,所以其語法和功能更為均衡全面。

微軟公司是全球最大的電腦軟體提供商,為了佔據開發者市場,進而在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捕獲到該型別的異常。你也可以通過在配置檔案中新增執行時節點來對全域性進行這樣的一個配置:

<runtime>
     <legacyCorruptedStateExceptionsPolicy enabled="true" />
</runtime>

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編譯成一個可執行的控制檯程式的。
我們新建個記事本,然後將下面程式碼複製上去。

複製程式碼

    using System;
    using System.IO;
    using System.Net.Sockets;
    using System.Text;
    class Program
    {
        static void Main()
        {
            string rootDirectory = Environment.CurrentDirectory;
            Console.WriteLine("開始連線,埠號:8090");
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 8090));
            socket.Listen(30);
            while (true)
            {
                Socket socketClient = socket.Accept();
                Console.WriteLine("新請求");
                byte[] buffer = new byte[4096];
                int length = socketClient.Receive(buffer, 4096, SocketFlags.None);
                string requestStr = Encoding.UTF8.GetString(buffer, 0, length);
                Console.WriteLine(requestStr);
                //
                string[] strs = requestStr.Split(new string[] { "\r\n" }, StringSplitOptions.None);
                string url = strs[0].Split(' ')[1];

                byte[] statusBytes, headerBytes, bodyBytes;

                if (Path.GetExtension(url) == ".jpg")
                {
                    string status = "HTTP/1.1 200 OK\r\n";
                    statusBytes = Encoding.UTF8.GetBytes(status);
                    bodyBytes = File.ReadAllBytes(rootDirectory + url);
                    string header = string.Format("Content-Type:image/jpg;\r\ncharset=UTF-8\r\nContent-Length:{0}\r\n", bodyBytes.Length);
                    headerBytes = Encoding.UTF8.GetBytes(header);
                }
                else
                {
                    if (url == "/")
                        url = "預設頁";
                    string status = "HTTP/1.1 200 OK\r\n";
                    statusBytes = Encoding.UTF8.GetBytes(status);
                    string body = "<html>" +
                        "<head>" +
                            "<title>socket webServer  -- Login</title>" +
                        "</head>" +
                        "<body>" +
                           "<div style=\"text-align:center\">" +
                               "當前訪問" + url +
                           "</div>" +
                        "</body>" +
                    "</html>";
                    bodyBytes = Encoding.UTF8.GetBytes(body);
                    string header = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n", bodyBytes.Length);
                    headerBytes = Encoding.UTF8.GetBytes(header);
                }
                socketClient.Send(statusBytes);
                socketClient.Send(headerBytes);
                socketClient.Send(new byte[] { (byte)'\r', (byte)'\n' });
                socketClient.Send(bodyBytes);

                socketClient.Close();
            }
        }
    }

複製程式碼

然後關閉記事本,將之.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型別引用表中找到該項:

注意圖,TypeRefName下面有該型別中被引用的成員,其標記值為0A000003,也就是get_CurrentDirectory了。
而從其ResolutionScope指向位於0x23000001而得之,該型別存在於mscorlib程式集。

於是我們開啟mscorlib.dll的元資料清單,可以在型別定義表(TypeDef)找到System.Environment,可以從元資料得知該型別的一些標誌(Flags,常見的public、sealed、class、abstract),也得知繼承(Extends)於System.Object。在該型別定義下還有型別的相關資訊,我們可以在其中找到get_CurrentDirectory方法。 我們可以得到該方法的相關資訊,這其中表明瞭該方法位於0x0002b784這個相對虛地址(RVA),接著JIT在新地址處理CIL,周而復始。

元資料在執行時的作用: https://docs.microsoft.com/zh-cn/dotnet/standard/metadata-and-self-describing-components#run-time-use-of-metadata

程式集的規則

上文我通過ILDASM來描述CLR執行程式碼的方式,但還不夠具體,還需要補充的是對於程式集的搜尋方式。

對於System.Environment型別,它存在於mscorlib.dll程式集中,demo.exe是個獨立的個體,它通過csc編譯的時候只是註冊了引用mscorlib.dll中的型別的引用資訊,並沒有記錄mscorlib.dll在磁碟上的位置,那麼,CLR怎麼知道get_CurrentDirectory的程式碼?它是從何處讀取mscorlib.dll的? 
對於這個問題,.NET有個專門的概念定義,我們稱為 程式集的載入方式。

程式集的載入方式

對於自身程式集內定義的型別,我們可以直接從自身程式集中的元資料中獲取,對於在其它程式集中定義的型別,CLR會通過一組規則來在磁碟中找到該程式集並載入在記憶體。

CLR在查詢引用的程式集的位置時候,第一個判斷條件是 判斷該程式集是否被簽名。
什麼是簽名?

強名稱程式集

就比如大家都叫張三,姓名都一樣,喊一聲張三不知道到底在叫誰。這時候我們就必須擴充套件一下這個名字以讓它具有唯一性。

我們可以通過sn.exe或VS對專案右鍵屬性在簽名選項卡中採取RSA演算法對程式集進行數字簽名(加密:公鑰加密,私鑰解密。簽名:私鑰簽名,公鑰驗證簽名),會將構成程式集的所有檔案通過雜湊演算法生成雜湊值,然後通過非對稱加密演算法用私鑰簽名,最後公佈公鑰生成一串token,最終將生成一個由程式集名稱、版本號、語言文化、公鑰組成的唯一標識,它相當於一個強化的名稱,即強名稱程式集。 
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

我們日常在VS中的專案預設都沒有被簽名,所以就是弱名稱程式集。強名稱程式集是具有唯一標識性的程式集,並且可以通過對比雜湊值來比較程式集是否被篡改,不過仍然有很多手段和軟體可以去掉程式集的簽名。

需要值得注意的一點是:當你試圖在已生成好的強名稱程式集中引用弱名稱程式集,那麼你必須對弱名稱程式集進行簽名並在強名稱程式集中重新註冊。 
之所以這樣是因為一個程式集是否被篡改還要考慮到該程式集所引用的那些程式集,根據CLR搜尋程式集的規則(下文會介紹),沒有被簽名的程式集可以被隨意替換,所以考慮到安全性,強名稱程式集必須引用強名稱程式集,否則就會報錯:需要強名稱程式集。

.NET Framework 4.5中對強簽名的更改:https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/enhanced-strong-naming

程式集搜尋規則

事實上,按照儲存位置來說,程式集分為共享(全域性)程式集和私有程式集。

CLR查詢程式集的時候,會先判斷該程式集是否被強簽名,如果強簽名了那麼就會去共享程式集的儲存位置(後文的GAC)去找,如果沒找到或者該程式集沒有被強簽名,那麼就從該程式集的同一目錄下去尋找。

強名稱程式集是先找到與程式集名稱(VS中對專案右鍵屬性應用程式->程式集名稱)相等的檔名稱,然後 按照唯一標識再來確認,確認後CLR載入程式集,同時會通過公鑰效驗該簽名來驗證程式集是否被篡改(如果想跳過驗證可查閱