原文連結:https://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和C#是什麼關系
- 跨語言和跨平台是什麼
- 什麼是跨語言互操作,什麼是CLS
- CLS異常
- 什麼是CTS?
- 什麼是類庫?
- 什麼是基礎類庫BCL?
- 什麼是架構類庫FCL?
- 什麼是基元類型?
- System.Object的意義
- 計算機是如何運作程式的?
- 什麼是CPU?
- 什麼是進階程式設計語言?
- 什麼是托管代碼,托管語言,托管子產品?
- 非托管的異常
- 什麼是CLR,.NET虛拟機?
- 什麼是CLR宿主程序,運作時主機?
- Windows系統自帶.NET Framework
- .NET Framework 4.0.30319
- .NET Framework4.X覆寫更新
- 如何确認本機安裝了哪些.NET Framework和對應CLR的版本?
- 什麼是程式集
- 用csc.exe進行編譯
- .NET程式執行原理
- JIT編譯
- AOT編譯
- 程式集的規則
- 程式集的加載方式
- 強名稱程式集
- 程式集搜尋規則
- 項目的依賴順序
- 為什麼Newtonsoft.Json版本不一緻?
- 如何在編譯時加載兩個相同的程式集
- 如何同時調用兩個兩個相同命名空間和類型的程式集?
- 共享程式集GAC
- 延伸
- 應用程式域
- 跨邊界通路
- AppDomain和AppPool
- 記憶體
- 堆棧和堆的差別
- 線程堆棧
- 為什麼值類型存儲在棧上
- 托管堆模型
- 選class還是struct
- GC管理器
- 弱引用、弱事件
- GC堆回收
- 垃圾回收對性能的影響
- 性能建議
- .NET程式執行圖
- .NET的安全性
- 基于角色的安全性
- 代碼通路安全性
- 什麼是.NET
- 什麼是.NET Framework
- 如何在VS中調試.NET Framework源代碼
- 什麼是.NET Core
- 什麼是.NET Standard
- .NET官方開源項目連結
- 什麼是.NET Framework
- Visual Studio
- sln解決方案
- 項目模闆
- csproj工程檔案
- 項目屬性雜項
- IntelliTrace智能追溯
- 連結
- 建議
.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編譯成一個可執行的控制台程式的。
我們建立個記事本,然後将下面代碼複制上去。
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()
這句話意思是調用 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加載程式集,同時會通過公鑰效驗該簽名來驗證程式集是否被篡改(如果想跳過驗證可查閱https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/how-to-disable-the-strong-name-bypass-feature),如果強名稱程式集被篡改則報錯。
而弱名稱程式集則直接按照與程式集名稱相等的檔案名稱來找,如果還是沒有找到就以該程式集名稱為目錄的檔案夾下去找。總之,如果最終結果就是沒找到那就會報System.IO.FileNotFoundException異常,即嘗試通路磁盤上不存在的檔案失敗時引發的異常。
注意:此處檔案名稱和程式集名稱是兩個概念,不要模棱兩可,檔案CLR頭内嵌程式集名稱。
舉個例子:
我有一個控制台程式,其路徑為D:\Demo\Debug\demo.exe,通過該程式的中繼資料得知,其引用了一個程式集名稱為aa的普通程式集,引用了一個名為bb的強名稱程式集,該bb.dll的強名稱辨別為:xx001。
現在CLR開始搜尋程式集aa,首先它會從demo.exe控制台的同一目錄(也就是D:\Demo\Debug)中查找程式集aa,搜尋檔案名為aa.dll的檔案,如果沒找到就在該目錄下以程式集名稱為目錄的目錄中查找,也就是會查 D:\Demo\Debug\aa\aa.dll,這也找不到那就報錯。
然後CLR開始搜尋程式集bb,CLR從demo.exe的中繼資料中發現bb是強名稱程式集,其辨別為:xx001。于是CLR會先從一個被定義為GAC的目錄中去通過辨別找,沒找到的話剩下的尋找步驟就和尋找aa一樣完全一緻了。
當然,你也可以通過配置檔案config中(配置檔案存在于應用程式的同一目錄中)人為增加程式集搜尋規則:
1.在運作時runtime節點中,添加privatePath屬性來添加搜尋目錄,不過隻能填寫相對路徑:
//程式集目前目錄下的相對路徑目錄,用;号分割
2.如果程式集是強簽名後的,那麼可以通過codeBase來指定網絡路徑或本地絕對路徑。
當然,我們還可以在代碼中通過AppDomain類中的幾個成員來改變搜尋規則,如AssemblyResolve事件、AppDomainSetup類等。
有關運作時節點的描述:https://docs.microsoft.com/zh-cn/dotnet/framework/configure-apps/file-schema/runtime/runtime-element
項目的依賴順序
如果沒有通過config或者在代碼中來設定CLR搜尋程式集的規則,那麼CLR就按照預設的也就是我上述所說的模式來尋找。
是以如果我們通過csc.exe來編譯項目,引用了其它程式集的話,通常需要将那些程式集複制到同一目錄下。故而每當我們通過VS編譯器對項目右鍵重新生成項目(重新編譯)時,VS都會将引用的程式集給複制一份到項目bin\輸出目錄Debug檔案夾下,我們可以通過VS中對引用的程式集右鍵屬性-複制本地 True/Flase 來決定這一預設行為。
值得一提的是,項目間的生成是有序生成的,它取決于項目間的依賴順序。
比如Web項目引用BLL項目,BLL項目引用了DAL項目。那麼當我生成Web項目的時候,因為我要注冊Bll程式集,是以我要先生成Bll程式集,而BLL程式集又引用了Dal,是以又要先生成Dal程式集,是以程式集生成順序就是Dal=>BLL=>Web,項目越多編譯的時間就越久。
程式集之間的依賴順序決定了編譯順序,是以在設計項目間的分層劃分時不僅要展現出層級職責,還要考慮到依賴順序。代碼存放在哪個項目要有講究,不允許出現互相引用的情況,比如A項目中的代碼引用B,B項目中的代碼又引用A。
為什麼Newtonsoft.Json版本不一緻?
而除了注意編譯順序外,我們還要注意程式集間的版本問題,版本間的錯亂會導緻程式的異常。
舉個經典的例子:Newtonsoft.Json的版本警告,大多數人都知道通過版本重定向來解決這個問題,但很少有人會琢磨為什麼會出現這個問題,找了一圈文章,沒找到一個解釋的。
比如:
A程式集引用了 C槽:\Newtonsoft.Json 6.0程式集
B程式集引用了 從Nuget下載下傳下來的Newtonsoft.Json 10.0程式集
此時A引用B,就會報:發現同一依賴程式集的不同版本間存在無法解決的沖突 這一警告。
A:引用Newtonsoft.Json 6.0
Func()
{
var obj= Newtonsoft.Json.Obj;
B.JsonObj();
}
B: 引用Newtonsoft.Json 10.0
JsonObj()
{
return Newtonsoft.Json.Obj;
}
A程式集中的Func方法調用了B程式集中的JsonObj方法,JsonObj方法又調用了Newtonsoft.Json 10.0程式集中的對象,那麼當執行Func方法時程式就會異常,報System.IO.FileNotFoundException: 未能加載檔案或程式集Newtonsoft.Json 10.0的錯誤。
這是為什麼?
1.這是因為依賴順序引起的。A引用了B,首先會先生成B,而B引用了 Newtonsoft.Json 10.0,那麼VS就會将源引用檔案(Newtonsoft.Json 10.0)複制到B程式集同一目錄(bin/Debug)下,名為Newtonsoft.Json.dll檔案,其内嵌程式集版本為10.0。
2.然後A引用了B,是以會将B程式集和B程式集的依賴項(Newtonsoft.Json.dll)給複制到A的程式集目錄下,而A又引用了C槽的Newtonsoft.Json 6.0程式集檔案,是以又将C:\Newtonsoft.Json.dll檔案給複制到自己程式集目錄下。因為兩個Newtonsoft.Json.dll重名,是以直接覆寫了前者,那麼隻保留了Newtonsoft.Json 6.0。
3.當我們調用Func方法中的B.Convert()時候,CLR會搜尋B程式集,找到後再調用 return Newtonsoft.Json.Obj 這行代碼,而這行代碼又用到了Newtonsoft.Json程式集,接下來CLR搜尋Newtonsoft.Json.dll,檔案名稱滿足,接下來CLR判斷其辨別,發現版本号是6.0,與B程式集清單裡注冊的10.0版本不符,故而才會報出異常:未能加載檔案或程式集Newtonsoft.Json 10.0。
以上就是為何Newtonsoft.Json版本不一緻會導緻錯誤的原因,其也诠釋了CLR搜尋程式集的一個過程。
那麼,如果我執意如此,有什麼好的解決方法能讓程式順利執行呢?有,有2個方法。
第一種:通過bindingRedirect節點重定向,即當找到10.0的版本時,給定向到6.0版本
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json"
publicKeyToken="30ad4fe6b2a6aeed"
culture="neutral" />
<bindingRedirect oldVersion="10.0.0.0"
newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
如何在編譯時加載兩個相同的程式集?
注意:我看過有的文章裡寫的一個AppDomain隻能加載一個相同的程式集,很多人都以為不能同時加載2個不同版本的程式集,實際上CLR是可以同時加載Newtonsoft.Json 6.0和Newtonsoft.Json 10.0的。
第二種:對每個版本指定codeBase路徑,然後分别放上不同版本的程式集,這樣就可以加載兩個相同的程式集。
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json"
publicKeyToken="30ad4fe6b2a6aeed"
culture="neutral" />
<codeBase version="6.0.0.0"
href="D:\6.0\Newtonsoft.Json.dll" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json"
publicKeyToken="30ad4fe6b2a6aeed"
culture="neutral" />
<codeBase version="10.0.0.0"
href="D:\10.0\Newtonsoft.Json.dll" />
</dependentAssembly>
</assemblyBinding>
</runtime>
如何同時調用兩個兩個相同命名空間和類型的程式集?
除了程式集版本不同外,還有一種情況就是,我一個項目同時引用了程式集A和程式集B,但程式集A和程式集B中的命名空間和類型名稱完全一模一樣,這個時候我調用任意一個類型都無法區分它是來自于哪個程式集的,那麼這種情況我們可以使用extern alias外部别名。
我們需要在所有代碼前定義别名,extern alias a;extern alias b;,然後在VS中對引用的程式集右鍵屬性-别名,分别将其更改為a和b(或在csc中通過/r:{别名}={程式集}.dll)。
在代碼中通過 {别名}::{命名空間}.{類型}的方式來使用。
extern-alias介紹: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/extern-alias
共享程式集GAC
我上面說了這麼多有關CLR加載程式集的細節和規則,事實上,類似于mscorlib.dll、System.dll這樣的FCL類庫被引用的如此頻繁,它已經是我們.NET程式設計中必不可少的一部分,幾盡每個項目都會引用,為了不再每次使用的時候都複制一份,是以計算機上有一個位置專門存儲這些我們都會用到的程式集,叫做全局程式集緩存(Global Assembly Cache,GAC),這個位置一般位于C:\Windows\Microsoft.NET\assembly和3.5之前版本的C:\Windows\assembly。
既然是共享存放的位置,那不可避免的會遇到檔案名重複的情況,那麼為了杜絕該類情況,規定在GAC中隻能存在強名稱程式集,每當CLR要加載強名稱程式集時,會先通過辨別去GAC中查找,而考慮到程式集檔案名稱一緻但版本文化等複雜的情況,是以GAC有自己的一套目錄結構。我們如果想将自己的程式集放入GAC中,那麼就必須先簽名,然後通過如gacutil.exe工具(其存在于指令行工具中 https://docs.microsoft.com/zh-cn/dotnet/framework/tools/developer-command-prompt-for-vs中)來注冊至GAC中,值得一提的是在将強名稱程式集安裝在GAC中,會效驗簽名。
GAC工具: https://docs.microsoft.com/en-us/dotnet/framework/tools/gacutil-exe-gac-tool
延伸
CLR是按需加載程式集的,沒有執行代碼也就沒有調用相應的指令,沒有相應的指令,CLR也不會對其進行相應的操作。 當我們執行Environment.CurrentDirectory這段代碼的時候,CLR首先要擷取Environment類型資訊,通過自身中繼資料得知其存在mscorlib.dll程式集中,是以CLR要加載該程式集,而mscorlib.dll又由于其地位特殊,早在CLR初始化的時候就已經被類型加載器自動加載至記憶體中,是以這行代碼可以直接在記憶體中讀取到類型的方法資訊。
在這個章節,我雖然描述了CLR搜尋程式集的規則,但事實上,加載程式集讀取類型資訊遠遠沒有這麼簡單,這涉及到了屬于.NET Framework獨有的"應用程式域"概念和記憶體資訊的查找。
簡單延伸兩個問題,mscorlib.dll被加載在哪裡?記憶體堆中又是什麼樣的一個情況?
應用程式域
傳統非托管程式是直接承載在Windows程序中,托管程式是承載在.NET虛拟機CLR上的,而在CLR中管控的這部分資源中,被分成了一個個邏輯上的分區,這個邏輯分區被稱為應用程式域,是.NET Framework中定義的一個概念。
因為堆記憶體的建構和删除都通過GC去托管,降低了人為出錯的幾率,在此特性基礎上.NET強調在一個程序中通過CLR強大的管理建立起對資源邏輯上的隔離區域,每個區域的應用程式互不影響,進而讓托管代碼程式的安全性和健壯性得到了提升。
熟悉程式集加載規則和AppDomain是在.NET技術下進行插件程式設計的前提。AppDomain這部分概念并不複雜。
當啟動一個托管程式時,最先啟動的是CLR,在這過程中會通過代碼初始化三個邏輯區域,最先是SystemDomain系統程式域,然後是SharedDoamin共享域,最後是{程式集名稱}Domain預設域。
系統程式域裡維持着一些系統建構項,我們可以通過這些項來監控并管理其它應用程式域等。共享域存放着其它域都會通路到的一些資訊,當共享域初始化完畢後,會自動加載mscorlib.dll程式集至該共享域。而預設域則用儲存自身程式集的資訊,我們的主程式集就會被加載至這個預設域中,執行程式入口方法,在沒有特殊動作外所産生的一切耗費都發生在該域。
我們可以在代碼中建立和解除安裝應用程式域,域與域之間有隔離性,挂掉A域不會影響到B域,并且對于每一個加載的程式集都要指定域的,沒有在代碼中指定域的話,預設都是加載至預設域中。
AppDomain可以想象成組的概念,AppDomain包含了我們加載的一組程式集。我們通過代碼解除安裝AppDomain,即同時解除安裝了該AppDomain中所加載的所有程式集在記憶體中的相關區域。
AppDomain的初衷是邊緣隔離,它可以讓程式不重新啟動而長時間運作,圍繞着該概念建立的體系進而讓我們能夠使用.NET技術進行插件程式設計。
當我們想讓程式在不關閉不重新部署的情況下添加一個新的功能或者改變某一塊功能,我們可以這樣做:将程式的主子產品仍預設加載至預設域,再建立一個新的應用程式域,然後将需要更改或替換的子產品的程式集加載至該域,每當更改和替換的時候直接解除安裝該域即可。 而因為域的隔離性,我在A域和B域加載同一個程式集,那麼A域和B域就會各存在記憶體位址不同但資料相同的程式集資料。
跨邊界通路
事實上,在開發中我們還應該注意跨域通路對象的操作(即在A域中的程式集代碼直接調用B域中的對象)是與平常程式設計中有所不同的,一個域中的應用程式不能直接通路另一個域中的代碼和資料,對于這樣的在程序内跨域通路操作分兩類。
一是按引用封送,需要繼承System.MarshalByRefObject,傳遞的是該對象的代理引用,與源域有相同的生命周期。
二是按值封送,需要被[Serializable]标記,是通過序列化傳遞的副本,副本與源域的對象無關。
無論哪種方式都涉及到兩個域直接的封送、解封,是以跨域通路調用不适用于過高頻率。
(比如,原來你是這樣調用對象: var user=new User(); 現在你要這樣:var user=(User){應用程式域對象執行個體}.CreateInstanceFromAndUnwrap(“Model.dll”,“Model.User”); )
值得注意的是,應用程式域是對程式集的組的劃分,它與程序中的線程是兩個一橫一豎,方向不一樣的概念,不應該将這2個概念放在一起比較。我們可以通過Thread.GetDomain來檢視執行線程所在的域。
應用程式域在類庫中是System.AppDomain類,部分重要的成員有:
擷取目前 System.Threading.Thread 的目前應用程式域
public static AppDomain CurrentDomain { get; }
使用指定的名稱建立應用程式域
public static AppDomain CreateDomain(string friendlyName);
解除安裝指定的應用程式域。
public static void Unload(AppDomain domain);
訓示是否對目前程序啟用應用程式域的 CPU 和記憶體監視,開啟後可以根據相關屬性進行監控
public static bool MonitoringIsEnabled { get; set; }
目前域托管代碼抛出異常時最先發生的一個事件,架構設計中可以用到
public event EventHandler<FirstChanceExceptionEventArgs> FirstChanceException;
當某個異常未被捕獲時調用該事件,如代碼裡隻catch了a異常,實際産生的是 b異常,那麼b異常就沒有捕捉到。
public event UnhandledExceptionEventHandler UnhandledException;
為指定的應用程式域屬性配置設定指定值。該應用程式域的局部存儲值,該存儲不劃分上下文和線程,均可通過GetData擷取。
public void SetData(string name, object data);
如果想使用托管代碼來覆寫CLR的預設行為https://msdn.microsoft.com/zh-cn/library/system.appdomainmanager(v=vs.85).aspx
public AppDomainManager DomainManager { get; }
傳回域的配置資訊,如在config中配置的節點資訊
public AppDomainSetup SetupInformation { get; }
應用程式域: https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/application-domains
AppDomain和AppPool
注意:此處的AppDomain應用程式域 和 IIS中的AppPool應用程式池 是2個概念,AppPool是IIS獨有的概念,它也相當于一個組的概念,對網站進行劃組,然後對組進行一些如程序模型、CPU、記憶體、請求隊列的進階配置。
記憶體
應用程式域把資源給隔離開,這個資源,主要指記憶體。那麼什麼是記憶體呢?
要知道,程式運作的過程就是電腦不斷通過CPU進行計算的過程,這個過程需要讀取并産生運算的資料,為此我們需要一個擁有足夠容量能夠快速與CPU互動的存儲容器,這就是記憶體了。對于記憶體大小,32位處理器,尋址空間最大為2的32次方byte,也就是4G記憶體,除去作業系統所占用的公有部分,程序大概能占用2G記憶體,而如果是64位處理器,則是8T。
而在.NET中,記憶體區域分為堆棧和托管堆。
堆棧和堆的差別
堆和堆棧就記憶體而言隻不過是位址範圍的差別。不過堆棧的資料結構和其存儲定義讓其在時間和空間上都緊密的存儲,這樣能帶來更高的記憶體密度,能在CPU緩存和分頁系統表現的更好。故而通路堆棧的速度總體來說比通路堆要快點。
線程堆棧
作業系統會為每條線程配置設定一定的空間,Windwos為1M,這稱之為線程堆棧。在CLR中的棧主要用來執行線程方法時,儲存臨時的局部變量和函數所需的參數及傳回的值等,在棧上的成員不受GC管理器的控制,它們由作業系統負責配置設定,當線程走出方法後,該棧上成員采用後進先出的順序由作業系統負責釋放,執行效率高。
而托管堆則沒有固定容量限制,它取決于作業系統允許程序配置設定的記憶體大小和程式本身對記憶體的使用情況,托管堆主要用來存放對象執行個體,不需要我們人工去配置設定和釋放,其由GC管理器托管。
為什麼值類型存儲在棧上
不同的類型擁有不同的編譯時規則和運作時記憶體配置設定行為,我們應知道,C# 是一種強類型語言,每個變量和常量都有一個類型,在.NET中,每種類型又被定義為值類型或引用類型。
使用 struct、enum 關鍵字直接派生于System.ValueType定義的類型是值類型,使用 class、interface、delagate 關鍵字派生于System.Object定義的類型是引用類型。
對于在一個方法中産生的值類型成員,将其值配置設定在棧中。這樣做的原因是因為值類型的值其占用固定記憶體的大小。
C#中int關鍵字對應BCL中的Int32,short對應Int16。Int32為2的32位,如果把32個二進制數排列開來,我們要求既能表達正數也能表達負數,是以得需要其中1位來表達正負,首位是0則為+,首位是1則為-,那麼我們能表示資料的數就隻有31位了,而0是介于-1和1之間的整數,是以對應的Int32能表現的就是2的31次方到2的31次方-1,即2147483647和-2147483648這個整數段。
1個位元組=8位,32位就是4個位元組,像這種以Int32為代表的值類型,本身就是固定的記憶體占用大小,是以将值類型放在記憶體連續配置設定的棧中。
托管堆模型
而引用類型相比值類型就有點特殊,newobj建立一個引用類型,因其類型内的引用對象可以指向任何類型,故而無法準确得知其固定大小,是以像對于引用類型這種無法預知的容易産生記憶體碎片的動态記憶體,我們把它放到托管堆中存儲。
托管堆由GC托管,其配置設定的核心在于堆中維護着一個nextObjPtr指針,我們每次執行個體(new)一個對象的時候,CLR将對象存入堆中,并在棧中存放該對象的起始位址,然後該指針都會根據該對象的大小來計算下一個對象的起始位址。不同于值類型直接在棧中存放值,引用類型則還需要在棧中存放一個代表(指向)堆中對象的值(位址)。
而托管堆又可以因存儲規則的不同将其分類,托管堆可以被分為3類:
- 1.用于托管對象執行個體化的垃圾回收堆,又以存儲對象大小分為小對象(<85000byte)的GC堆(SOH,Small Object Heap)和用于存儲大對象執行個體的(>=85000byte)大對象堆(LOG,Larage Object Heap)。
- 2.用于存儲CLR元件和類型系統的加載(Loader)堆,其中又以使用頻率分為經常通路的高頻堆(裡面包含有MethodTables方法表, MeghodDescs方法描述, FieldDescs方法描述和InterfaceMaps接口圖),和較低的低頻堆,和Stub堆(輔助代碼,如JIT編譯後修改機器代碼指令位址環節)。
- 3.用于存儲JIT代碼的堆及其它雜項的堆。
加載程式集就是将程式集中的資訊給映射在加載堆,對産生的執行個體對象存放至垃圾回收堆。前文說過應用程式域是指通過CLR管理而建立起的邏輯上的記憶體邊界,那麼每個域都有其自己的加載堆,隻有解除安裝應用程式域的時候,才會回收該域對應的加載堆。
而加載堆中的高頻堆包含的有一個非常重要的資料結構表—方法表,每個類型都僅有一份方法表(MethodTables),它是對象的第一個執行個體建立前的類加載活動的結果,它主要包含了我們所關注的3部分資訊:
- 1包含指向EEClass的一個指針。EEClass是一個非常重要的資料結構,當類加載器加載到該類型時會從中繼資料中建立出EEClass,EEClass裡主要存放着與類型相關的表達資訊。
- 2包含指向各自方法的方法描述器(MethodDesc)的指針邏輯組成的線性表資訊:繼承的虛函數, 新虛函數, 執行個體方法, 靜态方法。
- 3包含指向靜态字段的指針。
那麼,執行個體一個對象,CLR是如何将該對象所對應的類型行為及資訊的記憶體位置(加載堆)關聯起來的呢?
原來,在托管堆上的每個對象都有2個額外的供于CLR使用的成員,我們是通路不到的,其中一個就是類型對象指針,它指向位于加載堆中的方法表進而讓類型的狀态和行為關聯了起來, 類型指針的這部分概念我們可以想象成obj.GetType()方法獲得的運作時對象類型的執行個體。而另一個成員就是同步塊索引,其主要用于2點:1.關聯内置SyncBlock數組的項進而完成互斥鎖等目的。 2.是對象Hash值計算的輸入參數之一。
上述gif是我簡單畫的一個圖,可以看到對于方法中申明的值類型變量,其在棧中作為一塊值表示,我們可以直接通過c#運算符sizeof來獲得值類型所占byte大小。而方法中申明的引用類型變量,其在托管堆中存放着對象執行個體(對象執行個體至少會包含上述兩個固定成員以及執行個體資料,可能),在棧中存放着指向該執行個體的位址。
當我new一個引用對象的時候,會先配置設定同步塊索引(也叫對象頭位元組),然後是類型指針,最後是類型執行個體資料(靜态字段的指針存在于方法表中)。會先配置設定對象的字段成員,然後配置設定對象父類的字段成員,接着再執行父類的構造函數,最後才是本對象的構造函數。這個多态的過程,對于CLR來說就是一系列指令的集合,是以不能糾結new一個子類對象是否會也會new一個父類對象這樣的問題。而也正是因為引用類型的這樣一個特征,我們雖然可以估計一個執行個體大概占用多少記憶體,但對于具體占用的大小,我們需要專門的工具來測量。
對于引用類型,u2=u1,我們在指派的時候,實際上賦的是位址,那麼我改動資料實際上是改動該位址指向的資料,這樣一來,因為u2和u1都指向同一塊區域,是以我對u1的改動會影響到u2,對u2的改動會影響到u1。如果我想互不影響,那麼我可以繼承IClone接口來實作記憶體克隆,已有的CLR實作是淺克隆方法,但也隻能克隆值類型和String(string是個特殊的引用類型,對于string的更改,其會産生一個新執行個體對象),如果對包含其它引用類型的這部分,我們可以自己通過其它手段實作深克隆,如序列化、反射等方式來完成。而如果引用類型中包含有值類型字段,那麼該字段仍然配置設定在堆上。
對于值類型,a=b,我們在指派的時候,實際上是建立了個值,那麼我改動a的值那就隻會改動a的值,改動b的值就隻會改動b的值。而如果值類型(如struct)中包含的有引用類型,那麼仍是同樣的規則,引用類型的那部分執行個體在托管堆中,位址在棧上。
我如果将值類型放到引用類型中(如:object a=3),會在棧中生成一個位址,在堆中生成該值類型的值對象,還會再生成這類型指針和同步塊索引兩個字段,這也就是常說裝箱,反過來就是拆箱。每一次的這樣的操作,都會涉及到記憶體的分布、拷貝,可見,裝箱和拆箱是有性能損耗,是以應該減少值類型和引用類型之間轉換的次數。
但對于引用類型間的子類父類的轉換,僅是指令的執行消耗,幾盡沒有開銷。
選class還是struct
那麼我到底是該new一個class呢還是選擇struct呢?
通過上文知道對于class,用完之後對象仍然存在托管堆,占用記憶體。對于struct,用完之後直接由作業系統銷毀。那麼在實際開發中定義類型時,選擇class還是struct就需要注意了,要綜合應用場景來辨識。struct存在于棧上,棧和托管堆比較,最大的優勢就是即用即毀。是以如果我們單純的傳遞一個類型,那麼選擇struct比較合适。但須注意線程堆棧有容量限制,不可多存放超大量的值類型對象,并且因為是值類型直接傳遞副本,是以struct作為方法參數是線程安全的,但同樣要避免裝箱的操作。而相比較class,如果類型中還需要多一些封裝繼承多态的行為,那麼class當然是更好的選擇。
GC管理器
值得注意的是,當我new完一個對象不再使用的時候,這個對象在堆中所占用的記憶體如何處理?
在非托管世界中,可以通過代碼手動進行釋放,但在.NET中,堆完全由CLR托管,也就是說GC堆是如何具體來釋放的呢?
當GC堆需要進行清理的時候,GC收集器就會通過一定的算法來清理堆中的對象,并且版本不同算法也不同。最主要的則為Mark-Compact标記-壓縮算法。
這個算法的大概含義就是,通過一個圖的資料結構來收集對象的根,這個根就是引用位址,可以了解為指向托管堆的這根關系線。當觸發這個算法時,會檢查圖中的每個根是否可達,如果可達就對其标記,然後在堆上找到剩餘沒有标記(也就是不可達)的對象進行删除,這樣,那些不在使用的堆中對象就删除了。
前面說了,因為nextObjPtr的緣故,在堆中配置設定的對象都是連續配置設定的,因為未被标記而被删除,那麼經過删除後的堆就會顯得支零破碎,那麼為了避免空間碎片化,是以需要一個操作來讓堆中的對象再變得緊湊、連續,而這樣一個操作就叫做:Compact壓縮。
而對堆中的分散的對象進行挪動後,還會修改這些被挪動對象的指向位址,進而得以正确的通路,最後重新更新一下nextObjPtr指針,周而複始。
而為了優化記憶體結構,減少在圖中搜尋的成本,GC機制又為每個托管堆對象定義了一個屬性,将每個對象分成了3個等級,這個屬性就叫做:代,0代、1代、2代。
每當new一個對象的時候,該對象都會被定義為第0代,當GC開始回收的時候,先從0代回收,在這一次回收動作之後,0代中沒有被回收的對象則會被定義成第1代。當回收第1代的時候,第1代中沒有被清理掉的對象就會被定義到第2代。
CLR初始化時會為0/1/2這三代選擇一個預算的容量。0代通常以256 KB-4 MB之間的預算開始,1代的典型起始預算為512 KB-4 MB,2代不受限制,最大可擴充至作業系統程序的整個記憶體空間。
比如第0代為256K,第1代為2MB。我們不停的new對象,直到這些對象達到256k的時候,GC會進行一次垃圾回收,假設這次回收中回收了156k的不可達對象,剩餘100k的對象沒有被回收,那麼這100k的對象就被定義為第1代。現在就變成了第0代裡面什麼都沒有,第1代裡放的有100k的對象。這樣周而複始,GC清除的永遠都隻有第0代對象,除非當第一代中的對象累積達到了定義的2MB的時候,才會連同清理第1代,然後第1代中活着的部分再更新成第二代…
第二代的容量是沒有限制,但是它有動态的門檻值(因為等到整個記憶體空間已滿以執行垃圾回收是沒有意義的),當達到第二代的門檻值後會觸發一次0/1/2代完整的垃圾收集。
也就是說,代數越長說明這個對象經曆了回收的次數也就越多,那麼也就意味着該對象是不容易被清除的。
這種分代的思想來将對象分割成新老對象,進而配對不同的清除條件,這種巧妙的思想避免了直接清理整個堆的尴尬。
弱引用、弱事件
GC收集器會在第0代飽和時開始回收托管堆對象,對于那些已經申明或綁定的不經通路的對象或事件,因為不經常通路而且還占記憶體(有點懶加載的意思),是以即時對象可達,但我想在GC回收的時候仍然對其回收,當需要用到的時候再建立,這種情況該怎麼辦?
那麼這其中就引入了兩個概念:
WeakReference弱引用、WeakEventManager弱事件
對于這2兩個不區分語言的共同概念,大家可自行擴充百度,此處就不再舉例。
GC堆回收
那麼除了通過new對象而達到代的阈(臨界)值時,還有什麼能夠導緻垃圾堆進行垃圾回收呢? 還可能windows報告記憶體不足、CLR解除安裝AppDomain、CLR關閉等其它特殊情況。
或者,我們還可以自己通過代碼調用。
.NET有GC來幫助開發人員管理記憶體,并且版本也在不斷疊代。GC幫我們托管記憶體,但仍然提供了System.GC類讓開發人員能夠輕微的協助管理。 這其中有一個可以清理記憶體的方法(并沒有提供清理某個對象的方法):GC.Collect方法,可以對所有或指定代進行即時垃圾回收(如果想調試,需在release模式下才有效果)。這個方法盡量别用,因為它會擾亂代與代間的秩序,進而讓低代的垃圾對象跑到生命周期長的高代中。
GC還提供了,判斷目前對象所處代數、判斷指定代數經曆了多少次垃圾回收、擷取已在托管堆中配置設定的位元組數這樣的三個方法,我們可以從這3個方法簡單的了解托管堆的情況。
托管世界的記憶體不需要我們打理,我們無法從代碼中得知具體的托管對象的大小,你如果想追求對記憶體最細微的控制,顯然C#并不适合你,不過類似于有關記憶體把控的這部分功能子產品,我們可以通過非托管語言來編寫,然後通過.NET平台的P/Invoke或COM技術(微軟為CLR定義了COM接口并在系統資料庫中注冊)來調用。
像FCL中的源碼,很多涉及到作業系統的諸如 檔案句柄、網絡連接配接等外部extren的底層方法都是非托管語言編寫的,對于這些非托管子產品所占用的資源,我們可以通過隐式調用析構函數(Finalize)或者顯式調用的Dispose方法通過在方法内部寫上非托管提供的釋放方法來進行釋放。
像文中示例的socket就将釋放資源的方法寫入Dispose中,析構函數和Close方法均調用Dispose方法以此完成釋放。事實上,在FCL中的使用了非托管資源的類大多都遵循IDispose模式。而如果你沒有釋放非托管資源直接退出程式,那麼作業系統會幫你釋放該程式所占的記憶體的。
垃圾回收對性能的影響
還有一點,垃圾回收是對性能有影響的。
GC雖然有很多優化政策,但總之,隻要當它開始回收垃圾的時候,為了防止線程在CLR檢查期間對對象更改狀态,是以CLR會暫停程序中的幾乎所有線程(是以線程太多也會影響GC時間),而暫停的時間就是應用程式卡死的時間,為此,對于具體的處理細節,GC提供了2種配置模式讓我們選擇。
第一種為:單CPU的工作站模式,專為單CPU處理器定做。這種模式會采用一系列政策來盡可能減少GC回收中的暫停時間。
而工作站模式又分為并發(或背景)與不并發兩種,并發模式表現為響應時間快速,不并發模式表現為高吞吐量。
第二種為:多CPU的伺服器模式,它會為每個CPU都運作一個GC回收線程,通過并行算法來使線程能真正同時工作,進而獲得性能的提升。
我們可以通過在Config檔案中更改配置來修改GC模式,如果沒有進行配置,那麼應用程式總是預設為單CPU的工作站的并發模式,并且如果機器為單CPU的話,那麼配置伺服器模式則無效。
如果在工作站模式中想禁用并發模式,則應該在config中運作時節點添加
如果想更改至伺服器模式,則可以添加 。
gcConcurrent: https://docs.microsoft.com/zh-cn/dotnet/framework/configure-apps/file-schema/runtime/gcconcurrent-element
gcServer: https://docs.microsoft.com/zh-cn/dotnet/framework/configure-apps/file-schema/runtime/gcserver-element
性能建議
雖然我們可以選擇适合的GC工作模式來改善垃圾回收時的表現,但在實際開發中我們更應該注意減少不必要的記憶體開銷。
幾個建議是,減換需要建立大量的臨時變量的模式、考慮對象池、大對象使用懶加載、對固定容量的集合指定長度、注意字元串操作、注意高頻率的隐式裝箱操作、延遲查詢、對于不需要面向對象特性的類用static、需要高性能操作的算法改用外部元件實作(p/invoke、com)、減少throw次數、注意匿名函數捕獲的外部對象将延長生命周期、可以閱讀GC相關運作時配置在高并發場景注意變換GC模式…
對于.NET中改善性能可延伸閱讀 https://msdn.microsoft.com/zh-cn/library/ms973838.aspx 、 https://msdn.microsoft.com/library/ms973839.aspx
.NET程式執行圖
至此,.NET Framework上的三個重要概念,程式集、應用程式域、記憶體在本文講的差不多了,我畫了一張圖簡單的概述.NET程式的一個執行流程:
對于後文,我将單獨的介紹一些其它雜項,首先是.NET平台的安全性。
.NET的安全性
.NET Framework中的安全機制分為 基于角色的安全機制 和 代碼通路安全機制 。
基于角色的安全性
基于角色的安全機制作為傳統的通路控制,其運用的非常廣泛,如作業系統的安全政策、資料庫的安全政策等等…它的概念就相當于我們經常做的那些RBAC權限管理系統一樣,使用者關聯角色,角色關聯權限,權限對應着操作。
整個機制的安全邏輯就和我們平時編寫代碼判斷是一樣的,大緻可以分為兩個步驟.
第一步就是建立一個主體,然後辨別這個主體是什麼身份(角色) ,第二步就是 身份驗證,也就是if判斷該身份是否可以這樣操作。
而在.NET Framework中,這主體可以是Windows賬戶,也可以是自定義的辨別,通過生成如目前線程或應用程式域使用的主體相關的資訊來支援授權。
比如,構造一個代表目前登入賬戶的主體對象WindowsPrincipal,然後通過 AppDomain.CurrentDomain.SetThreadPrincipal(主體對象);或Thread.CurrentPrincipal的set方法來設定應用程式域或線程的主體對象, 最後使用System.Security.Permissions.PrincipalPermission特性來标記在方法上來進行授權驗證。
如圖,我目前登入賬号名稱為DemoXiaoZeng,然後通過Thread.CurrentPrincipal設定目前主體,執行aa方法,順利列印111。如果檢測到PrincipalPermission類中的Name屬性值不是目前登入賬号,那麼就報錯:對主體權限請求失敗。
在官方文檔中有對.NET Framework基于角色的安全性的詳細的介紹,感興趣可以去了解 https://docs.microsoft.com/zh-cn/dotnet/standard/security/principal-and-identity-objects#principal-objects
代碼通路安全性
在.NET Framework中還有一個安全政策,叫做 代碼通路安全Code Access Security,也就是CAS了。
代碼通路安全性在.NET Framework中是用來幫助限制代碼對受保護資源和操作的通路權限。
舉個例子,我通過建立一個FileIOPermission對象來限制對後續代碼對D盤的檔案和目錄的通路,如果後續代碼對D盤進行資源操作則報錯。
FileIOPermission是代碼控制通路檔案和檔案夾的能力。除了FileIOPermission外,還有如PrintingPermission代碼控制通路列印機的權限、RegistryPermission代碼控制操作系統資料庫的權限、SocketPermission控制接受連接配接或啟動Socket連接配接的權限。
對于這些通過代碼來對受保護資源和操作的權限限制,也就是這些類名字尾為Permission的類,它們叫做 Permissions(權限),都繼承自CodeAccessPermission,都有如Demand,Assert,Deny,PermitOnly,IsSubsetOf,Intersect和Union這些方法,在MSDN上有完整的權限清單:https://msdn.microsoft.com/en-us/library/h846e9b3(v=vs.100).aspx
為了确定代碼是否有權通路某一資源或執行某一操作,CLR的安全系統将稽核調用堆棧,以将每個調用方獲得的權限與要求的權限進行比較。 如果調用堆棧中的任何調用方不具備要求的權限,則會引發安全性異常并拒絕通路。
圖出自 https://docs.microsoft.com/zh-cn/dotnet/framework/misc/code-access-security
而除了Permissions權限,代碼通路安全性機制還有 權限集、證據、代碼組、政策等概念。這些概念讓CAS如此強大,但相應的,它們也讓CAS變得複雜,必須為每個特定機器定義正确的PermissionSet和Code Groups才能設定成一個成功的CAS政策。
考慮到這層原因,Microsoft .NET安全小組決定從頭開始重建代碼通路安全性。在.NET Framework4.0之後,就不再使用之前的那套CAS模型了,而是使用.NET Framework 2.0中引入的安全透明模型,然後稍加修改,修改後的安全透明模型成為保護資源的标準方法,被稱之為:安全透明度級别2
安全透明度2介紹:https://msdn.microsoft.com/en-us/library/dd233102(v=vs.100).aspx
.NET Framework4.0的安全更改:https://msdn.microsoft.com/en-us/library/dd233103(v=vs.100).aspx
一個完整的CAS示範:https://www.codeproject.com/Articles/5724/Understanding-NET-Code-Access-Security
對于安全透明度級别2我将不再介紹,感興趣的可以看我推薦的這2篇文章,對Level2的安全透明度介紹的比較詳細,包括實踐、遷移。
https://www.red-gate.com/simple-talk/dotnet/.net-framework/whats-new-in-code-access-security-in-.net-framework-4.0—part-i/
https://www.red-gate.com/simple-talk/dotnet/net-framework/whats-new-in-code-access-security-in-net-framework-4-0-part-2/
須注意:
.NET平台上的安全機制,僅僅是.NET平台上的,是以它隻限制于托管代碼,我們可以直接調用非托管代碼或程序通信間接調用非托管代碼等多個手段來突破對托管代碼 操作資源的限制。
事實上,我們在平常項目中代碼編寫的安全機制(業務邏輯身份驗證、項目架構驗證)與這些平台級的安全機制沒什麼不同。我們可以了解為代碼寫的位置不一樣,.NET安全機制是寫在CLR元件中,而我們的安全機制是寫在上層的代碼中。這些平台級的辨別更多的是和作業系統使用者有關,而我們項目代碼中的辨別則是和在資料庫中注冊的使用者有關, 大家都是通過if else來去判斷,判斷的主體和格局不一樣,邏輯本質都是相同的。
NET Core不支援代碼通路安全性和安全性透明性。
.NET是什麼
我在前文對.NET系統概述時,有的直接稱.NET,有的稱.NET Framework。那麼準确來說什麼是.NET?什麼又是.NET Framework呢?
.NET是一個微軟搭造的開發者平台,它主要包括:
- 1.支援(面向)該平台的程式設計語言(如C#、Visual Basic、C++/CLI、F#、IronPython、IronRuby…),
- 2.用于該平台下開發人員的技術架構體系(.NET Framework、.NET Core、Mono、UWP等),
- 1.定義了通用類型系統,龐大的CTS體系
- 2.用于支撐.NET下的語言運作時的環境:CLR
- 3…NET體系技術的架構庫FCL
- 3.用于支援開發人員開發的軟體工具(即SDK,如VS2017、VS Code等)
.NET Framework是什麼
事實上,像我上面講的那些諸如程式集、GC、AppDomain這樣的為CLR的一些概念組成,實質上指的是.NET Framework CLR。
.NET平台是微軟為了占據開發市場而成立的,不是無利益驅動的純技術平台的那種東西。基于該平台下的技術架構也因為 商業間的利益 進而和微軟自身的Windows作業系統所綁定。是以雖然平台雄心和口号很大,但很多架構類庫技術都是以Windows系統為藍本,這樣就導緻,雖然.NET各方面都挺好,但是用.NET就必須用微軟的東西,直接形成了技術-商業的綁定。
.NET Framework就是.NET 技術架構組成在Windows系統下的具體的實作,和Windows系統高度耦合,上文介紹的.NET系統,就是指.NET Framework。
部署.net Framework :https://docs.microsoft.com/zh-cn/dotnet/framework/deployment/deployment-guide-for-developers
.NET Framework進階開發:https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2008/29eafad8(v%3dvs.90)
.NET Framework源碼線上浏覽:https://referencesource.microsoft.com/
如何在VS中調試.NET Framework源代碼
最為關鍵的是pdb符号檔案,沒得符号就調不了,對于符号我們從微軟的符号伺服器上下載下傳(預設就已配置),還得有源代碼來調試。
點選工具-選項-調試-正常,如果你之前沒有在該配置欄配置過,那麼你就勾選 啟用源伺服器支援 、啟用.net Framework源代碼單步執行,然後将 要求源檔案與原始版本完全比對 給取消掉。
然後就是下載下傳pdb符号檔案了,如果想直接下載下傳那麼可以在調試-符号這欄 将Microsoft符号伺服器給勾上 。如果想按需下載下傳,那麼在調試的時候,可以點選調試-視窗 選擇 子產品/調用堆棧 來選擇自己想加載的去加載。
然後至 https://referencesource.microsoft.com/網站 點選右上角下載下傳源代碼。當你調試代碼的時候,會提示你無可用源,這個時候你再将你下載下傳下來的源碼檔案給浏覽查找一下就可以了。
如何配置VS來調試.NET Framework源碼: https://referencesource.microsoft.com/#q=web 、 https://technet.microsoft.com/zh-cn/cc667410.aspx
還一種方法是,下載下傳.NET Reflector插件,該插件可以幫助我們在VS中直接調試dll,這種方式操作非常簡單,不過該插件收費,具體的可以檢視我之前寫過的文章(群裡有該插件的注冊版)
.NET Core是什麼
有醜才有美,有低才有高,概念是比較中誕生的。.NET Core就是如此,它是其它作業系統的.NET Framework翻版實作。
作業系統不止Windows,還有Mac和類Linux等系統, .NET的實作 如果按作業系統來橫向分割的話,可以分為 Windows系統下的 .NET Framework 和 相容多個作業系統的 .NET Core。
我們知道,一個.NET程式運作核心在于.NET CLR,為了能讓.NET程式在其它平台上運作,一些非官方社群群組織為此開發了在其它平台下的.NET實作(最為代表的是mono,其團隊後來又被微軟給合并了 ),但因為不是官方,是以在一些方面多少有些缺陷(如FCL),後來微軟官方推出了.NET Core,其開源在Github中,并被收錄在NET基金會(.NET Foundation,由微軟公司成立與贊助的獨立自由軟體組織,其目前收錄包括.NET編譯器平台(“Roslyn”)以及ASP.NET項目系列,.NET Core,Xamarin Forms以及其它流行的.NET開源架構),旨在真正的 .NET跨平台。
.NET Core是.NET 技術架構組成在Windows.macOS.Linux系統下的具體的實作。
.NET Core是一個開源的項目,其由 Microsoft 和 GitHub 上的 .NET 社群共同維護,但 這份工作仍然是巨大的,因為在早期對.NET上的定義及最初的實作一直是以Windows系統為參照及載體,一些.NET機制實際上與Windows系統耦合度非常高,有些屬于.NET自己體系内的概念,有些則屬于Windows系統api的封裝。 那麼從Windows轉到其它平台上,不僅要實作相應的CLR,還要舍棄或重寫一部分BCL,因而,.NET Core在概念和在項目中的行為與我們平常有些不同。
比如,NET Core不支援AppDomains、遠端處理、代碼通路安全性 (CAS) 和安全透明度,任何有關該概念的庫代碼都應該被替換。
這部分代碼它不僅指你項目中的代碼,還指你項目中using的那些程式集代碼,是以你會在github上看到很多開源項目都在跟進對.NET Core的支援,并且很多開發者也嘗試學習.NET Core,這也是一種趨勢。
.NET Core指南https://docs.microsoft.com/en-us/dotnet/core/
.NET基金會:https://dotnetfoundation.org
.NET Core跨平台的行為變更:https://github.com/dotnet/corefx/wiki/ApiCompat
微軟宣布.NET開發環境将開源 :https://news.cnblogs.com/n/508410/
.NET Standard是什麼
值得一提的是微軟還為BCL提出了一個标準,畢竟各式各樣的平台,技術層出不窮,為了防止.NET在類庫方面的碎片化,即提出了一套正式的 .NET API (.NET 的應用程式程式設計接口)規範,.NET Standard。
正如上面CLS一樣,.NET Standard就類似于這樣的一個概念,無論是哪個托管架構,我們遵循這個标準,就能始終保持在BCL的統一性,即我不需要關心我是用的.NET Framework還是.NET Core,隻要該類被定義于.NET Standard中,我就一定能在對應支援的.NET Standard的版本的托管架構中找到它。
.NET Standard: https://docs.microsoft.com/zh-cn/dotnet/standard/net-standard#net-implementation-support
.NET Standard開源代碼:https://github.com/dotnet/standard
.NET官方開源項目連結
現在我将給出.NET相關的開源項目位址:
參與.NET和.NET開源項目的起點:https://github.com/Microsoft/dotnet
- .NET Core:https://github.com/dotnet/core
- .NET Core文檔:https://github.com/dotnet/docs
- ASP.NET Core:https://github.com/aspnet/home
- ASP.NET Core文檔:https://github.com/aspnet/Docs
- EntityFramework Core架構:https://github.com/aspnet/EntityFrameworkCore
- ASP.NET Core MVC架構:https://github.com/aspnet/Mvc
- EntityFramework6:https://github.com/aspnet/EntityFramework6
- .NET Framework源碼:https://github.com/microsoft/referencesource
- .NET Core基類庫:https://github.com/dotnet/corefx
- .NET Core CLR:https://github.com/dotnet/coreclr
- Roslyn編譯器:https://github.com/dotnet/roslyn
- MVC5、Web API2、Web Pages3架構源碼:https://github.com/aspnet/AspNetWebStack
- .NET Standard:https://github.com/dotnet/standard
- KestrelHttpServer用于ASP.NET Core的跨平台Web伺服器:https://github.com/aspnet/KestrelHttpServer
- Visual Studio Code源碼:https://github.com/Microsoft/vscode
- 一些優秀的.NET庫、工具、架構、軟體開源集合:https://github.com/quozd/awesome-dotnet
- 一些常用架構對ASP.NET Core和.NET Core的支援報告:https://github.com/jpsingleton/ANCLAFS
- 一些.NET下用于支援開發的開源項目集合:https://github.com/Microsoft/dotnet/blob/master/dotnet-developer-projects.md
- 微軟出品的分布式架構orleans:https://github.com/dotnet/orleans
- ML.NET 用于.NET的開源和跨平台機器學習架構:https://github.com/dotnet/machinelearning
Visual Studio
在文章最後,我還要簡單的說下Visual Studio。
通過上文得知,隻需要一個txt記事本+csc.exe我們就可以開發出一個.NET程式,那麼與之相比,.NET提供的開發工具VS有什麼不同呢?
我們用記事本+csc.exe來編寫一個.NET程式隻适合小打小鬧,對于真正要開發一個項目而言,我們需要檔案管理、版本管理、一個好的開發環境等。而vs ide則就是這樣一個內建代碼編輯、編譯、調試、追蹤、測試、部署、協作、插件擴充這樣多個元件的內建開發環境,csc.exe的編譯功能隻是vs ide中的其中之一。使用vside開發可以節省大量的開發時間和成本。
sln解決方案
當你用VS來建立一個項目時,VS會先為你建立一個整體的解決方案。這個解決方案表現為.sln和.suo字尾格式的檔案,它們均是文本檔案,對解決方案右鍵屬性可以進行相應的修改,也可以直接用記事本打開。
在sln中,定義了解決方案的版本及環境,如包含的項目,方案啟動項,生成或部署的一些項目配置等,你可以通過修改或重新定義sln來更改你的整個解決方案。
而suo則包含于解決方案建立關聯的選項,相當于快照,儲存了使用者界面的自定義配置、調試器斷點、觀察視窗設定等這樣的東西,它是隐藏檔案,可删除但建議不要删除。
我們可以通過對比各版本之間的sln來修改sln,也可以使用網上的一些轉換工具,也可以直接點選VS的檔案-建立-從現有代碼建立項目來讓項目在不同VS版本間切換。
Visual Studio 2010 - # Visual Studio 4.0
Visual Studio 2012 - # Visual Studio 4.0
Visual Studio 2013 - # Visual Studio 12.00
Visual Studio 2015 - # Visual Studio 14
Visual Studio 2017 - # Visual Studio 15
項目模闆
VS使用項目模闆來基于使用者的選擇而建立新的項目,也就是建立項目中的那些展示項(如mvc5項目/winform項目等等),具體表現為包含.vstemplate及一些定義的關聯檔案這樣的母版檔案。将這些檔案壓縮為一個 .zip 檔案并放在正确的檔案夾中時,就會在展示項中予以顯示。
使用者可以建立或自定義項目模闆,也可以選擇現有的模闆,比如我建立一個控制台項目就會生成一個在.vstemplate中定義好的Program.cs、AssemblyInfo.cs(程式集級别的特性)、App.config、ico、csproj檔案
csproj工程檔案
這裡面,csproj是我們最常見的核心檔案,CSharp Project,它是用于建構這個項目的工程檔案。
csproj是基于xml格式的MSBuild項目檔案,其仍然是文本檔案,可以打開并修改定義了的工程構造的屬性,比如選擇性的添加或删除或修改包含在項目中的檔案或引用、修改項目版本、将其轉換為其它類型項目等。
MSBuild是微軟定義的一個用于生成應用程式的平台(Microsoft Build Engine),在這裡為VS提供了項目的構造系統,在微軟官方文檔上有着詳細的說明:https://msdn.microsoft.com/zh-cn/library/dd393573.aspx、https://docs.microsoft.com/zh-cn/visualstudio/msbuild/msbuild
項目屬性雜項
現在,簡單說明一下csproj檔案的一些核心元素。我們用vs建立一個控制台項目,然後對項目右鍵屬性打開項目屬性,在應用程式頁我們可以定義:程式集名稱(生成出來的程式集以程式集名稱作為檔案名,相當于csc中的/out)、預設命名空間(每次建立類裡面顯示的命名空間)、目标架構、應用程式類型、程式集資訊(AssemblyInfo中的資訊)、啟動對象(可同時存在多個Main方法,需指定其中一個為入口對象)、程式集資源(一些可選的圖示及檔案)
1.在生成頁有:
- 條件編譯符号(全局的預編譯#define指令,不用在每個檔案頭部定義,相當于csc中的/define)
- 定義DEBUG/TRACE常量(用于調試輸出的定義變量,如智能追蹤的時候可以輸出該變量)
- 目标平台(指定目前面向什麼處理器生成的程式集,相當于csc中的/platform。選擇x86則生成的程式集生成32位程式,能在32/64位Intel處理器中使用。選擇x64則生成64位,隻能在64位系統中運作。選擇Any CPU則32位系統生成32位,64位系統則生成64位。注意:編譯平台和目标調用平台必須保持一緻,否則報錯。生成的32位程式集不能調用64位程式集,64位也不能調用32位)、首選32位(如果目标平台是Any CPU并且項目是應用程式類型,則生成的是32位程式集)
- 允許不安全代碼(unsafe開關,在c#中進行指針程式設計,如調換a方法和b方法的位址)
- 優化代碼(相當于csc中的/optimize,優化IL代碼讓調試難以進行,優化JIT代碼)
- 輸出路徑(程式集輸出目錄,可選擇填寫相對路徑目錄或絕對路徑目錄)
- XML文檔檔案(相當于csc中的/doc,為程式集生成文檔注釋檔案,浏覽對方程式集對象就可以看到相關注釋,VS的智能提示技術就運用于此)
- 為COM互操作注冊(訓示托管應用程式将公開一個 COM 對象,使COM對象可以與托管應用程式進行互動)
2.在進階生成設定中有:語言版本(可以選擇C#版本)、調試資訊(相當于csc中的/debug。選擇none則不生成任何調試資訊,無法調試。選擇full則允許将調試器附加到運作程式,生成pdb調試檔案。選擇pdb-only,自.NET2.0開始與full選項完全相同,生成相同的pdb調試檔案。)、檔案對齊(指定輸出檔案中節的大小)、DLL基址(起點位址)
3.在生成事件選項中可以設定生成前和生産後執行的指令行,我們可以執行一些指令。
4.在調試選項中有一欄叫:啟用Visual Studio承載程序,通過在vshost.exe中加載運作項目程式集,這個選項可以增加程式的調試性能,啟用後會自動在輸出目錄生成{程式集名稱}.vshost.exe這樣一個檔案,隻有當目前項目不是啟動項目的時候才能删除該檔案。
IntelliTrace智能追溯
還要介紹一點VS的是,其IntelliTrace智能追溯功能,該功能最早存在于VS2010旗艦版,是我用的最舒服的一個功能。
簡單介紹,該功能是用來輔助調試的,在調試時可以讓開發人員了解并追溯代碼所産生的一些事件,并且能夠進行回溯以檢視應用程式中發生的情形,它是一個非常強大的調試追蹤器,它可以捕捉由你代碼産生的事件,如異常事件、函數調用(從入口)、ADO.NET的指令(Sql查詢語句…)、ASP.NET相關事件、代碼發送的HTTP請求、程式集加載解除安裝事件、檔案通路打開關閉事件、Winform/Webform/WPF動作事件、線程事件、環境變量、Console/Trace等輸出…
我們可以通過在調試狀态下點選調試菜單-視窗-顯示診斷工具,或者直接按Ctrl+Alt+F2來喚起該功能視窗。
當然,VS還有其它強大的功能,我建議大家依次點完 菜單項中的 調試、體系結構、分析這三個大菜單裡面的所有項,你會發現VS真是一個強大的IDE。比較實用且友善的功能舉幾個例子:
比如 從代碼生成的序列圖,該功能在vs2015之前的版本可以找到(https://msdn.microsoft.com/en-us/library/dd409377.aspx 、https://www.zhihu.com/question/36413876)
比如 子產品關系的代碼圖,可以看到各子產品間的關系
比如 對解決方案的代碼度量分析結果
比如 調試狀态下 函數調用的 代碼圖,我們可以看到MVC架構的函數管道模型
以及并行堆棧情況、加載的子產品、線程的實際情況
還有如程序、記憶體、反彙編、寄存器等的功能,這裡不再一一展示
連結
有關解決方案:https://msdn.microsoft.com/zh-cn/library/b142f8e7(v=vs.110).aspx
有關項目模闆: https://msdn.microsoft.com/zh-cn/library/ms247121(v=vs.110).aspx
有關項目元素的說明介紹:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/16satcwx(v%3dvs.100)
有關調試更多内容:https://docs.microsoft.com/zh-cn/visualstudio/debugger/
有關代碼設計建議:https://docs.microsoft.com/zh-cn/visualstudio/code-quality/code-analysis-for-managed-code-warnings
有關IntelliTrace介紹:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/dd264915(v%3dvs.100)