1. 程式人生 > >.NET Core跨平臺的奧祕[下篇]:全新的佈局

.NET Core跨平臺的奧祕[下篇]:全新的佈局

從本質上講,按照CLI規範設計的.NET從其出生的那一刻就具有跨平臺的基因,這與Java別無二致。由於採用了統一的中間語言,微軟只需要針對不同的平臺設計不同的虛擬機器(執行時)就能彌合不同作業系統與處理器架構之間的差異,但是“理想很豐滿,現實很骨感”。在過去十多年中,微軟將.NET引入到了各個不同的應用領域,表面上看起來似乎欣欣向榮,但是由於採用完全獨立的多目標框架的設計思路,導致針對多目標框架的程式碼平臺只能通過PCL(參考《.NET Core跨平臺的奧祕[中篇]:複用之殤》)這種“妥協”的方式來解決。如果依然按照這條道路走下去,.NET的觸角延伸得越廣,枷鎖將越來越多,所以.NET 已經到了不得不做出徹底改變的時刻了。

一、跨平臺的.NET Core

綜上所述,要真正實現.NET 的跨平臺偉業,主要需要解決兩個問題,一是針對不同的平臺設計相應的執行時為中間語言CIL提供一個一致性的執行環境,而是提供統一的BCL以徹底解決程式碼複用的難題。對於真正跨平臺的.NET Core來說,微軟不僅為它設計了針對不同平臺被成為CoreCLR的執行時,同時還重新設計了一套被稱為CoreFX的BCL。

2-221

如上圖所示,NET Core目前支援的AppModel主要有兩種,其中ASP.NET Core用於開發伺服器Web應用和服務,而UWP(Universal Windows Platform)則用於開發能夠在各種客戶端裝置(Mobile、PC、Xbox、Devices + IOT、HoloLens和Surface Hub等)上以自適應方式執行的Windows 10應用。CoreFX

是經過完全重寫的BCL,除了自身就具有跨平臺執行的能力之外,其提供的API也不再是統一定義在少數幾個單一的程式集中,而是經過有效分組之後被定義在各自獨立的模組中。這些模組對應著一個單一的程式集,並最終由對應的NuGet包來分發。至於底層的虛擬機器,微軟則為主流的作業系統型別(Windows、Mac OS X和Linux)和處理器架構(x86、x64和ARM)設計了針對性的執行時,被稱為CoreCLR

作為執行時的CoreCLR和提供BCL的CoreFX是.NET Core兩根重要的基石,但是就開發成本來看,微軟在後者投入的精力是前者無法比擬的。我們知道.NET Core自誕生到現在已經有好些年了,目前的版本還只是到了2.0,從釋出進度上顯得稍顯緩慢,其中一個主要的原因是:重寫CoreFX提供的基礎API確實是一件繁瑣耗時的工程,而且這項工程遠未結束。為了對CoreFX提供的BCL有一個大致的瞭解,我們看看這些常用的基礎API究竟定義在哪些名稱空間下。

  • System.Collections:定義了我們常用的集合型別。
  • System.Console:提供API完成基本的控制檯操作。
  • System.Data:提供用於訪問資料庫的API,相當於原來的ADO.NET。
  • System.Diagnostics:提供基本的診斷、除錯和追蹤的API。
  • System.DirectoryServices:提供基於AD(Active Directory)管理的API。
  • System.Drawing:提供GDI相關的API。
  • System.Globalization:提供API實現多語言以及全球化支援。
  • System.IO:提供針對檔案輸入輸出相關的API。
  • System.Net:提供與網路通訊相關的API。
  • System.Reflection:提供API以實現與反射相關的操作。
  • System.Runtime:提供與執行時相關的一些基礎型別。
  • System.Security:提供與資料簽名和加解密相關的API。
  • System.Text:提供針對字串/文字編碼與解碼相關的API。
  • System.Threading:提供用於管理執行緒的API。
  • System.Xml:提供API用以操作XML結構的資料。

我們知道對於傳統的.NET Framework來說,承載BCL的API幾乎都定義在mscorlib.dll這個程式集中,這些API並不是全部都轉移到組成CoreFX的眾多程式集中,那些與執行時(CoreCLR)具有緊密關係的底層API被定義到一個叫做System.Private.CoreLib.dll的程式集中,所以下圖反映了真正的.NET Core層次結構。我們在程式設計過程中使用的基礎資料型別基本上都定義在這個程式集中,所以目前這個程式集的尺寸已經超過了10M。由於該程式集提供的API與執行時關聯較為緊密,較之CoreFX提供的API,這些基礎API具有較高的穩定性,所以它是隨著CoreCLR一起釋出的。

2-23

雖然我們程式設計過程中使用到的絕大部分基礎型別都定義在System.Private.CoreLib.dll程式集中,但是這卻是一個“私有”的程式集,我們可以從其命名看出這一點。我們將System.Private.CoreLib.dll稱為一個私有程式集,並不是說定義其中的都是一些私有型別,而是因為我們在程式設計的過程不會真正引用這個程式集,這與.NET Framework下的mscorlib.dll是不一樣的。不僅如此,當我們編寫的.NET Core程式碼被編譯的時候,編譯器也不會連結到這個程式集上,也就是說編譯後生成的程式集中同樣也沒有針對該程式集引用的元資料。但是當我們的應用被真正執行的時候,所有引用的基礎型別全部會自動 “轉移” 到這個程式集中。至於如何實現執行過程中的型別轉移,其實就是利用了我們上面介紹的Type Forwarding技術。

例項演示:針對System.Private.CoreLib.dll程式集的型別轉移

對上面介紹的針對System.Private.CoreLib.dll程式集的型別轉移,可能很多人還是難以理解,為了讓大家對這個問題具有徹底的認識,我們不妨來做一個簡單的例項演示。我們利用Visual Studio建立一個.NET Core控制檯應用,並在作為程式入口的Main方法中編寫如下幾行程式碼,它們會將我們常用的幾個資料型別(System.String、System.Int32和System.Boolean)所在的程式集名稱列印在控制檯上。

   1: class Program
   2: {
   3:     static void Main()
   4:     {
   5:         Console.WriteLine(typeof(string).Assembly.FullName);
   6:         Console.WriteLine(typeof(int).Assembly.FullName);
   7:         Console.WriteLine(typeof(bool).Assembly.FullName);
   8:     }
   9: }

根據我們上面的分析,程式執行過程中使用到的這些基礎型別全部來源於System.Private.CoreLib.dll這個程式集中,關於這一點在如下圖所示的輸出結果中得到了證實。我們通過圖2-24所示的輸出結果,我們不僅僅知道了這個核心程式集的名稱,還知道了該程式集目前的版本(4.0.0.0);

2-24

我們說應用編譯後生成的程式集並不會具有針對System.Private.CoreLib.dll程式集引用的元資料,為了證明這一點,我們只需要利用Windows SDK(在目錄“%ProgramFiles(x86)%Microsoft SDKs\Windows\{version}\Bin”下)提供的反編譯工具ildasm.exe就可以了。利用ildasm.exe開啟這個控制檯應用編譯後生成的程式集之後,我們會發現它具有如下這兩個程式集的應用。

   1: .assembly extern System.Runtime
   2: {
   3:   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         
   4:   .ver 4:2:0:0
   5: }
   6: .assembly extern System.Console
   7: {
   8:   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         
   9:   .ver 4:1:0:0
  10: }

實際上我們的程式只涉及到四個型別,即一個Console型別和三個基礎資料型別(String、Int32和Boolean),而程式集層面則只有針對System.Runtime和System.Console程式集的引用,那麼毫無疑問,後面這三個資料型別肯定與System.Runtime程式集有關,那麼該程式集針對這三個資料型別具有怎樣的定義呢?為了得到答案,我們先得知道這個程式集究竟被儲存在哪裡。我們知道“%ProgramFiles%dotnet\”是.NET Core的應用根目錄,而這個System.Runtime.dll作為“共享”程式集被儲存在子目錄“\shared\Microsoft.NETCore.App\2.0.0”下面,這個目錄下面還儲存著很多其他的共享程式集。

我們依然利用反編譯工具ildasm.exe檢視System.Runtime.dll程式集清單檔案的元資料定義。我們會發現整個程式集除了定義少數幾個核心型別(比如兩個重要的委託型別Action和Func就定義在這個程式集中),它的作用就是將所有基礎的型別採用Type Forwarding的方式轉移到System.Private.CoreLib.dll程式集中,下面的程式碼片段為你展示了針對我們程式使用的三個基礎資料型別轉移的相關定義。

   1: .assembly extern System.Private.CoreLib
   2: {
   3:   .publickeytoken = (7C EC 85 D7 BE A7 79 8E )         
   4:   .ver 4:0:0:0
   5: }
   6: .class extern forwarder System.String
   7: {
   8:   .assembly extern System.Private.CoreLib
   9: }
  10: .class extern forwarder System.Int32
  11: {
  12:   .assembly extern System.Private.CoreLib
  13: }
  14: .class extern forwarder System.Boolean
  15: {
  16:   .assembly extern System.Private.CoreLib
  17: }

我們演示例項體現的程式集直接的引用關係,以及如上程式碼片段體現的相關基礎型別(System.String、System.Int32和System.Boolean)的轉移方向基本體現在如下圖所示的關係圖中。

2-25

複用.NET Framework程式集

我們將上述這種利用Type Forwarding方式實現跨程式集型別轉移的技術成為“墊片(Shim)”,這是實現程式集跨平臺複用的重要手段。除了System.Runtime.dll,.NET Core還提供了其他一些其他墊片程式集,正是源於這這些墊片程式集的存在,我們可以將在.NET Framework環境下編譯的程式集在.NET Core應用中使用。為了讓讀者朋友們對此有深刻的認識,我們照例來做一個簡單的例項演示。

我們利用Visual Studio建立一個空的解決方案,並新增如下三個專案(NetApp、NetCoreApp、NetLib),其中NetApp和NetCoreApp分別是針對.NET Framework(4.7)和.NET Core(2.0)的控制檯程式,而NetLib則是針對.NET Framework的類庫專案,該專案定義的API將在NetApp和NetCoreApp被呼叫。

2-26

我們在NetLib專案中定義了一個Utils工具類,並在其中定義了一個PrintAssemblyNames方法。如下面的程式碼片段所示,我們在這個方法中打印出三個常用的型別(Task、Uri和XmlWriter)所在的程式集的名稱。通過在不同型別(.NET Framework和.NET Core)的應用中呼叫這個方法,我們就可以確定它們在執行時究竟是從那個程式集中載入的。我們分別在NetApp和NetCoreApp這兩個不同型別的控制檯程式中呼叫了這個方法。

NetLib:

   1: public class Utils
   2: {
   3:     public static void PrintAssemblyNames()
   4:     {           
   5:         Console.WriteLine(typeof(Task).Assembly.FullName);
   6:         Console.WriteLine(typeof(Uri).Assembly.FullName);
   7:         Console.WriteLine(typeof(XmlWriter).Assembly.FullName);
   8:     }
   9: }

NetApp:

   1: class Program
   2: {
   3:     static void Main()
   4:     {
   5:         Console.WriteLine(".NET Framework 4.7");
   6:         Utils.PrintAssemblyNames();
   7:     }
   8: }

NetCoreApp:

   1: class Program
   2: {
   3:     static void Main()
   4:     {
   5:         Console.WriteLine(".NET Core 2.0");
   6:         Utils.PrintAssemblyNames();
   7:     }
   8: }

直接執行NetApp和NetCoreApp這兩個控制檯程式後,我們會發現不同的輸出結果。如下圖所示,對於我們指定的三個型別(System.Threading.Tasks.Task、System.Uri和System.Xml.XmlWriter),分別在.NET Framework和.NET Core環境下承載它們的程式集是不同的。具體來說,.NET Framework環境下的這三個型別分別定義在mscorlib.dll、System.dll和System.Xml.dll中;當切換到.NET Core環境下後,執行時則會從三個私有的程式集System.Private.CoreLib.dll、System.Private.Uri.dll和System.Private.Xml.dll中載入這三個型別。

2-27

由於NetApp和NetCoreApp這兩個控制檯應用使用的都是同一個針對.NET Framework編譯的程式集NetLib.dll,所以我們先利用反編譯工具ildasm.exe檢視一下它具有怎樣的程式集引用。如下面的程式碼片段所示,程式集NetLib.dll引用的程式集與控制檯應用NetApp的輸出結果是一致的。

   1: .assembly extern mscorlib
   2: {
   3:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   4:   .ver 4:0:0:0
   5: }
   6: .assembly extern System
   7: {
   8:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   9:   .ver 4:0:0:0
  10: }
  11: .assembly extern System.Xml
  12: {
  13:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
  14:   .ver 4:0:0:0
  15: }

那麼我們的核心問題變成了:Task、Uri和XmlWriter這三個型別在.NET Core的執行環境下是如何轉移到其他程式集中的。要回答這個問題,我們只需要利用ildasm.exe檢視mscorlib.dll、System.dll和System.Xml.dll反編譯這三個程式集就可以了。這三個程式集同樣存在於“%ProgramFiles%dotnet\\shared\Microsoft.NETCore.App\2.0.0”目錄下,通過反編譯與它們相關的程式集,我們得到如下所示的相關元資料。

mscorlib.dll

   1: .assembly extern System.Private.CoreLib
   2: {
   3:   .publickeytoken = (7C EC 85 D7 BE A7 79 8E )                         
   4:   .ver 4:0:0:0
   5: }
   6: .class extern forwarder System.Threading.Tasks.Task
   7: {
   8:   .assembly extern System.Private.CoreLib
   9: }

System.dll

   1: .assembly extern System.Private.Uri
   2: {
   3:   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         
   4:   .ver 4:0:4:0
   5: }
   6: .class extern forwarder System.Uri
   7: {
   8:   .assembly extern System.Private.Uri
   9: }

System.Xml.dll

   1: .assembly extern System.Xml.ReaderWriter
   2: {
   3:   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         
   4:   .ver 0:0:0:0
   5: }
   6: .class extern forwarder System.Xml.XmlWriter
   7: {
   8:   .assembly extern System.Xml.ReaderWriter
   9: }

System.Xml.ReaderWriter.dll

   1: .assembly extern System.Private.Xml
   2: {
   3:   .publickeytoken = (CC 7B 13 FF CD 2D DD 51 )                         
   4:   .ver 4:0:0:0
   5: }
   6: .class extern forwarder System.Xml.XmlWriter
   7: {
   8:   .assembly extern System.Private.Xml
   9: }

如上面的程式碼片段所示,針對Task、Uri和XmlWriter這三個型別的轉移一共涉及到七個程式集,其中mscorlib.dll、System.dll和System.Xml.dll是NetLib.dll直接引用的三個程式集,而System.Private.CoreLib.dll、System.Private.Uri.dll和System.Private.Xml.dll則是最終承載這三個型別的程式集。對於Task和Uri型別來說,它們只經歷一次轉移,而XmlWriter則經歷了兩次型別轉移,它轉移到程式集System.Xml.ReaderWriter.dll中,再借助後者轉移到目標程式集System.Private.Xml.dll,程式集引用和型別轉移關係體現在下圖中。

2-28

二、多平臺複用的BCL

雖然.NET Core藉助於CoreCLR和CoreFX實現了真正的跨平臺,但是目前的.NET Core僅僅提供ASP.NET Core和UWP這兩種程式設計模型,雖然後者旨在實現多種裝置的統一程式設計,但依然還是關注於Windows平臺。對於傳統.NET Framework下面向桌面應用的WPF和Windows Forms,它們並沒有跨平臺的意義,所以依然是今後.NET的一大分支。除此之外,雖然我們有了跨平臺的ASP.NET Core,傳統的ASP.NET依然被保留了下來,並且在今後一段時間內還將繼續升級。除了.NET Framework和.NET Core,.NET還具有另一個重要的分支,那就是Xamarin,它可以幫助我們為iOS、OS X和Android編寫統一的應用。在.NET誕生十多年後,微軟開始對.NET進行了全新的佈局,建立了 “大一統” 的.NET平臺。總的來說,這個所謂的大一統.NET平臺由如下圖所示的.NET Framework、.NET Core和Xamarin這三個分支組成。

2-29

雖然被微軟重新佈局的.NET平臺只包含了三個分支,但是之前遇到的一個重要的問題依然存在,那就是程式碼的複用,說的更加具體的是應該是程式集的複用而不是原始碼的複用。我們知道之前解決程式集服務的方案就是PCL,但這並不是一種理想的解決方案,由於各個目標框架具有各種獨立的BCL,所以我們建立的PCL專案只能建立在指定的幾種相容目標框架的BCL交集之上。對於全新的.NET平臺來說,這個問題通過提供統一的BCL得到根本的解決,這個統一的BCL被稱為.NET Standard

我們可以將.NET Standard稱為新一代的PCL,PCL提供的可移植能力僅僅限於建立時就確定下來的幾種目標平臺,但是.NET Standard做得更加徹底,因為它在設計的時候就已經考慮針對三大分支的複用。如下圖所示,.NET Standard為.NET Framework、.NET Core和Xamarin提供了統一的API,那麼我們在這組標準API基礎上編寫的程式碼自然就能被所有型別的.NET應用複用。

2-30

.NET Standard提供的API主要是根據現有.NET Framework來定義的,它的版本升級反映了其提供的API不斷豐富的過程,目前最新版本(.NET Standard 2.0)提供的API數量在前一版本基礎上幾乎翻了一番。Visual Studio提供相應的專案模板幫助我們建立基於.NET Standard的類庫專案,這樣的專案會採用專門的目標框架別名netstandard{version}。一個針對.NET Standard 2.0的類庫專案具有如下的定義,我們可以看到它採用的目標框架別名為 “.NET Standard 2.0” 。

   1: <Project Sdk="Microsoft.NET.Sdk">
   2:   <PropertyGroup>
   3:     <TargetFramework>netstandard2.0</TargetFramework>
   4:   </PropertyGroup>
   5: </Project>

顧名思義,.NET Standard僅僅是一個標準,而不提供具體的實現。我們可以簡單理解為.NET Standard為我們定義了一整套標準的介面,各個分支需要針對自身的執行環境對這套介面提供實現。對於.NET Core來說,它的基礎API主要由CoreFX和System.Private.CoreLib.dll這個核心程式集來承載,這些API基本上就是根據.NET Standard來設計的。但是對.NET Framework來說,它的BCL提供的API與.NET Standard存在著很大的交集,實際上.NET Standard基本上就是根據.NET Framework現有的API來設計的,所以微軟不可能在.NET Framework上重寫一套型別於CoreFX的實現,只需要採用某個技術 “連結” 到現有的程式集上就可以了。

一個針對.NET Standard編譯生成的程式集在不同的執行環境中針對真正提供實現的程式集的所謂“連結”依然是通過上面我們介紹的“墊片”技術來實現的,為了徹底搞清楚這個問題,我們還是先來作一個簡單的例項演示。如下圖所示,我們建立了與上面演示例項具有類似結構的解決方案,與之不同的是,分別針對.NET Framework和.NET Core的控制檯應用NetApp和NetCoreApp共同引用的類庫NetStandardLib是一個.NET Standard 2.0類庫專案。

2-31

與上面演示的例項一樣,我們在NetStandardLib中定義瞭如下一個Utils類,並利用定義其中的靜態方法PrintAssemblyNames資料兩個資料型別(Dictionary<,>和SortedDictionary<,>)所在的程式集名稱,該方法分別在NetApp和NetCoreApp的入口Main方法中被呼叫。

NetStandardLib:

   1: public class Utils
   2: {
   3:     public static void PrintAssemblyNames()
   4:     {           
   5:         Console.WriteLine(typeof(Dictionary<,>).Assembly.FullName);
   6:         Console.WriteLine(typeof(SortedDictionary<,>).Assembly.FullName);
   7:     }
   8: }

NetApp:

   1: class Program
   2: {
   3:     static void Main()
   4:     {
   5:         Console.WriteLine(".NET Framework 4.7");
   6:         Utils.PrintAssemblyNames();
   7:     }
   8: }

NetCoreApp:

   1: class Program
   2: {
   3:     static void Main()
   4:     {
   5:         Console.WriteLine(".NET Core 2.0");
   6:         Utils.PrintAssemblyNames();
   7:     }
   8: }

直接執行這兩個分別針對.NET Framework和.NET Core的控制檯應用NetApp和NetCoreApp,我們會發現它們會生成不同的輸出結果。如下圖所示,在.NET Framework和.NET Core 執行環境下,Dictionary<,>和SortedDictionary<,>這另個泛型字典型別其實來源於不同的程式集。具體來說,我們常用的Dictionary<,>型別在.NET Framework 4.7和.NET Core 2.0環境下分別定義在程式集mscorlib.dll和System.Private.CoreLib.dll中,而SortedDictionary<,>所在的程式集則分別是System.dll和System.Collection.dll。

2-32

對於演示的這個例項來說,這個NetStandardLib類庫專案針對的目標框架為.NET Standard 2.0,後者最終體現為一個名為NetStandard.Library.nupkg的NuGet包,這一點其實可以從Visual Studio針對該專案的依賴節點可以看出來。如下圖所示,這個名為NetStandard.Library的NuGet包具有一個核心的程式集netstandard.dll,上面我們所說的.NET Standard API就定義在該程式集中。

2-33

也就是說,所有.NET Standard 2.0專案都具有針對程式集netstandard.dll的依賴,這個依賴自然也會體現在編譯後生成的程式集上。對於我們演示例項中的這個類庫專案NetStandardLib編譯生成的同名程式集來說,它針對程式集netstandard.dll的依賴體現在如下所示的元資料中。

   1: .assembly extern netstandard
   2: {
   3:   .publickeytoken = (CC 7B 13 FF CD 2D DD 51 )                         
   4:   .ver 2:0:0:0
   5: }
   6: .assembly NetStandardLib
   7: {
   8:   ...
   9: }
  10: ...

按照我們即有的知識,原本定義在netstandard.dll的兩個型別(Dictionary<,>和SortedDictionary<,>)在不同過的執行環境中需要被轉移到另一個程式集中,我們完全可以在相應的環境中提供一個同名的墊片程式集並藉助型別的跨程式集轉移機制來實現,實際上微軟也就是這麼做的。我們先來看看針對.NET Framework的墊片程式集netstandard.dll的相關定義,我們可以直接在NetApp編譯的目標目錄中找到這個程式集。藉助於反編譯工具ildasm.exe,我們可以很容易地得到與Dictionary<,>和SortedDictionary<,>這兩個泛型字典型別轉移的相關元資料,具體的內容下面的程式碼片段所示。

   1: .assembly extern mscorlib
   2: {
   3:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   4:   .ver 0:0:0:0
   5: }
   6: .assembly extern System
   7: {
   8:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   9:   .ver 0:0:0:0
  10: }
  11: .class extern forwarder System.Collections.Concurrent.ConcurrentDictionary`2
  12: {
  13:   .assembly extern mscorlib
  14: }
  15: .class extern forwarder System.Collections.Generic.SortedDictionary`2
  16: {
  17:   .assembly extern System
  18: }

針對.NET Core的墊片程式集netstandard.dll被儲存在我們前面提到的共享目錄“%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\2.0.0”下,我們採用同樣的方式提取出與Dictionary<,>和SortedDictionary<,>這兩個泛型字典型別轉移的元資料。從如下的程式碼片段我們可以清晰地看出,Dictionary<,>和SortedDictionary<,>這兩個型別都被轉移到程式集System.Collections.dll之中。

   1: .assembly extern System.Collections
   2: {
   3:   .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         
   4:   .ver 0:0:0:0
   5: }
   6: .class extern forwarder System.Collections.Generic.Dictionary`2
   7: {
   8:   .assembly extern System.Collections
   9: }
  10: .class extern forwarder System.Collections.Generic.SortedDictionary`2
  11: {
  12:   .assembly extern System.Collections
  13: }

從演示例項的執行結果我們知道,SortedDictionary<,>確實是定義在程式集System.Collections.dll中,但是我們常用的Dictionary<,>型別則出自核心程式集System.Private.CoreLib.dll,那麼我們可以斷定Dictionary<,>型別在System.Collections.dll中必然出現了二次轉移。為了確認我們的斷言,我們只需要採用相同的方式反編譯程式集System.Collections.dll,該程式集也被儲存在共享目錄 “%ProgramFiles%dotnet\shared\Microsoft.NETCore.App\2.0.0” 中,該程式集中針對Dictionary<,>型別的轉移體現在如下所示的元資料中。

   1: .assembly extern System.Private.CoreLib
   2: {
   3:   .publickeytoken = (7C EC 85 D7 BE A7 79 8E )                         
   4:   .ver 4:0:0:0
   5: }
   6: .class extern forwarder System.Collections.Generic.Dictionary`2
   7: {
   8:   .assembly extern System.Private.CoreLib
   9: }

上面針對Dictionary<,>和SortedDictionary<,>這兩個型別分別在.NET Framework 4.7和.NET Core環境下的跨程式集轉移路徑基本上體現在下圖之中。簡單來說,.NET Framework環境下的墊片程式集netstandard.dll將這兩個型別分別轉移到了程式集mscorlib.dll和System.dll之中。如果執行環境切換到了.NET Core,這兩個型別先被轉移到System.Collection.dll中,但是Dictionary<,>這個常用型別最終是由System.Private.CoreLib.dll這個基礎程式集承載的,所有System.Collection.dll中針對該型別作了二次轉移。

2-34

上面這個簡單的型別基本上揭示了.NET Standard為什麼能夠提供全平臺的可移植性,我們現在來對此做一個簡單的總結。.NET Standard API由NetStandard.Library這個NuGet包來承載,後者提供了一個名為netstandard.dll的程式集,保留在這個程式集中的僅僅是. NET Standard API的存根(Stub),而不提供具體的實現。所有對於一個目標框架為.NET Standard的類庫專案編譯生成的程式集來說,它們保留了針對程式集netstandard.dll的引用。

.NET平臺的三大分支(.NET Framework、.NET Core和Xamarin)按照自己的方式各自實現了.NET Standard規定的這套標準的API。由於在執行時真正承載.NET Standard API的型別被分佈到多個程式集中,所以. NET Standard程式集能夠被複用的前提是執行時能夠將這些基礎型別連結到對應的程式集上。由於. NET Standard程式集是針對netstandard.dll進行編譯的,所以我們只需要在各自環境中提供這個同名的程式集來完成型別的轉移即可。

我的部落格即將搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan