1. 程式人生 > >.NET Core跨平臺的奧祕[中篇]:複用之殤

.NET Core跨平臺的奧祕[中篇]:複用之殤

在《.NET Core跨平臺的奧祕[上篇]:歷史的枷鎖》中我們談到:由於.NET是建立在CLI這一標準的規範之上,所以它天生就具有了“跨平臺”的基因。在微軟釋出了第一個針對桌面和伺服器平臺的.NET Framework之後,它開始 “樂此不疲” 地對這個完整版的.NET Framework進行不同範圍和層次的 “閹割” ,進而造就了像Windows Phone、Windows Store、Silverlight和.NET Micro Framework的壓縮版的.NET Framework。從這個意義上講,Mono和它們並沒有本質的區別,唯一不同的是Mono真正突破了Windows平臺的藩籬。包括Mono在內的這些分支促成了.NET的繁榮,但我們都知道這僅僅是一種虛假的繁榮而已。雖然都是.NET Framework的子集,但是由於它們採用完全獨立的執行時和基礎類庫,這使我們很難開發一個支援多種裝置的“可移植(Portable)”應用,這些分支反而成為制約.NET發展的一道道枷鎖。至於為什麼“可移植(Portable)”.NET應用的開發如此繁瑣呢?

所謂由於目標框架的獨立性,意味著不僅僅是作為虛擬機器的Runtime是根據具體平臺特性設計的,作為程式設計基礎的BCL也不能跨平臺共享,它為開發者帶來的一個最大的問題就是:很難編寫能夠在各個目標框架複用的程式碼。比較極端的場景就是:當我們需要為一個現有的桌面應用提供針對移動裝置的支援時,我們不得不從頭到尾開發一個全新的應用,現有的程式碼難以被新的應用所複用用。 “程式碼複用”是軟體設計一項最為根本的目標,在不考慮跨平臺的前提下,我們可以應用相應的設計模式和程式設計技巧來實現程式碼的重用,但是平臺之間的差異導致了跨平臺程式碼重用確實具有不小的困難。雖然作得不算非常的理想,但是微軟在這方面確實做出了很多嘗試,我們不妨先來聊聊目前我們都有哪些跨平臺程式碼複用的解決方案。

目錄
一、原始碼複用
    原始檔共享
    檔案連結
    共享專案
二、程式集複用
    程式集一致性
    Retargetable程式集
    型別的轉移
三、可移植類庫(PCL)

一、原始碼複用

對於包括Mono在內的各個.NET Framework平臺的BCL來說,雖然在API定義層面上存在一些共同之處,但是由於它們定義在不同的程式集之中,所以在PCL(Portal Class Library)推出之前,針對程式集的共享是不可能實現的,我們只能在原始碼層面實現共享。原始碼的共享通過在不同專案之間共享原始檔的方式來實現,至於具體採用的方式,我們有三種不同的方案供你選擇。

原始檔共享

對於一個能夠多個針對不同目標框架的專案共享的原始檔,定義其中的程式碼也有不少是針對具體某個目標框架的。對於這種程式碼,我們需要按照如下的方式進行編寫,相應的專案以新增編譯的方式選擇與自身平臺相匹配的程式碼編譯道生成的程式集中。

   1: #if WINDOWS
   2:     <<針對Windows Desktop>>
   3: #elif SILVERLIGHT
   4:     <<針對 Silverlight>>
   5: #elif WINDOWS_PHONE
   6:     <<針對Windows Phone>>
   7: #else
   8:     <<針對其他平臺>>
   9: #endif

如果多個針對不同.NET Framework平臺的專案檔案存在於同一個物理目錄下,存在於相同目錄下的原始檔可以同時包含到這些專案中以實現共享的目的。如下圖所示,兩個分別針對SilverlightWPF的專案共享相同的目錄,與兩個專案檔案同在一個目錄下的C#檔案Shared.cs可以同時被包含到這兩個專案之中。

2-9_thumb[2] 

檔案連結

當我們採用預設的方式將一個現有的檔案新增到當前專案之中的時候,Visual Studio會將目標檔案拷貝到專案本地的目錄下,所以根本起不到共享的目的。但是針對現有檔案的新增支援一種叫做“連結”的方式使新增到專案中的檔案指向的依然是原來的地址,我們可以為多個專案新增針對同一個檔案的連結以實現原始檔跨專案共享。同樣還是上面演示分別針對Silverlight和WPF的兩個專案,不論專案檔案和需要被共享的檔案存在於哪個目錄下面,我們都可以採用如下圖所示的新增檔案連結的方式分享這個Shared.cs檔案。

2-10_thumb[2]

共享專案(Shared Project)

普通專案的目的都是組織原始檔和其他相關資源並將它們最終編譯成一個可被部署的程式集。但是Shared Project這種專案型別則比較特別,它只有對原始檔進行組織的功能,卻不能通過編譯生成程式集,它存在的目的就是為了實現原始檔的共享。對於上面我們介紹的兩種原始碼的共享方式來說,它們都是針對某個單一檔案的共享,而Shared Project則可以對多個原始檔進行打包以實現批量共享。

2-11_thumb[2]

如上圖所示,我們可以建立一個Shared Project型別的專案Shared.shproj,並將需要共享的三個C#檔案(Foo.cs、Bar.cs和Baz.cs)新增進來。我們將針對這個專案的引用同時新增到一個Silverlight專案(SilverlightApp.csproj)和Windows Phone專案(WinPhoneApp.csproj)之中,當我們對這兩個專案實施編譯的時候,包含在專案Shared.shproj中的三個C#檔案會自動作為當前專案的原始檔參與編譯。  

二、程式集複用

我們採用C#、VB.NET這樣的程式語言編寫的原始檔經過編譯會生成有IL程式碼和元資料構成的託管模組,一個或者多個託管模組合併生成一個程式集。程式集的檔名、版本、語言文化和簽名的公鑰令牌共同組成了它的唯一標識,我們將該標識稱為程式集有效名稱(Assembly Qualified Name)。除了包含必要的託管模組之外,我們還可以將其他檔案作為資源內嵌到程式集中,程式集的檔案構成一個“清單(Manifest)”檔案來描述,這個清單檔案包含在某個託管模組中。

除了作為描述程式集檔案構造清單之外,描述程式集的元資料也包含在這個清單檔案中。程式集使程式整合為一個自描述性(Self-Describing)的部署單元,除了描述定義在本程式集中所有型別之外,這些元資料還包括對引用自外部程式集的描述。包含在元資料中針對外部程式集的描述是由編譯時引用的程式集決定的,引用程式集的名稱(包含檔名、版本和簽名的公鑰令牌)會直接體現在當前程式集的元資料中。針對程式集引用的元資料採用如下的形式(“.assembly extern”)被記錄在清單檔案中,我們可以看出被記錄下來的不僅包含被引用的程式集檔名(“Foo”和“Bar”),還包括程式集的版本,對於簽名的程式集(“Foo”)來說,公鑰令牌也一併包含其中。

   1: .assembly extern Foo
   2: {
   3:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   4:   .ver 1:0:0:0
   5: }
   6: .assembly extern Bar
   7: {
   8:   .ver 1:0:0:0
   9: }

包含在當前程式集清單檔案中針對引用程式集的元資料是CLR載入目標程式集的依據。在預設的情況下,CLR要求載入與程式集引用元資料完全一致的程式集。具體來說,如果引用的是一個未簽名的程式集(“Bar”),那麼只要求被載入的程式集具有一致的檔名和版本;如果引用的是一個經過簽名的程式集,那麼還要求被載入的程式集具有一致的公鑰令牌。

在回到《.NET Core跨平臺的奧祕[上篇]:歷史的枷鎖》關於.NET多目標框架獨立性的問題。雖然不同的目標框架的BCL在API層面具有很多交集,但是這些API實際上被定義在不同的程式集中,這就導致了在不同的目標框架下共享同一個程式集幾乎成了不可能的事情。如果要使跨目標平臺程式集複用成為現實,就必須要求CLR在載入程式集時放寬“完全匹配”的限制,因為針對當前程式集清單檔案中描述的某個引用程式集來說,在不同的目標框架下可能指向不同的程式集。實際上確實存在這樣的一些機制或者策略讓CLR載入一個與引用元資料的描述不一致的程式集,我們現在就來聊聊這些策略。

程式集一致性

我們都知道.NET Framework是向後相容的,也就是說原來針對低版本.NET Framework編譯生成的程式集是可以直接在高版本CLR下執行的。我們試想一下這麼一個問題:就一個針對.NET Framework 2.0編譯生成的程式集自身來說,所有引用的基礎程式集的版本在元資料描述中都應該是2.0,如果這個程式集在NET Framework 4.0環境下執行,CLR在決定載入它所依賴程式集的時候,應該選擇2.0還是4.0呢?

我們不妨通過實驗來獲得這個問題的答案。我們利用Visual Studio建立一個針對.NET Framework 2.0的控制檯應用(命名為App),並在作為程式入口的Main方法上編寫如下一段程式碼。如下面程式碼片斷所示,我們在控制檯上輸出了三個基本型別(Int32、XmlDocument和DataSet)所在程式集的全名。

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

直接執行這段程式使之在預設版本的CLR(2.0)下執行會在控制檯上輸出如下的結果,我們會發現上述三個基本型別所在程式集的版本都是2.0.0.0。也就說在這種情況下,執行時載入的程式集和編譯時引用的程式集是一致的。

2-11_thumb4

現在我們在目錄“\bin\debug”直接找到以Debug模式編譯生成的程式集App.exe,並按照如下的形式修改對應的配置檔案(App.exe.config),該配置的目的在於將啟動應用時採用的執行時(CLR)版本從預設的2.0切換到4.0。

   1: <configuration>
   2:   <startup>
   3:     <supportedRuntime&nbsp;version="v4.0"/>
   4:   </startup>
   5: </configuration>

或者:

   1: <configuration>
   2:   <startup>
   3:     <requiredRuntime&nbsp;version="v4.0"/>
   4:   </startup>
   5: </configuration>

無需重新編譯(確保執行的依然是同一個程式集)直接執行App.exe,我們會在控制檯上得到如下圖所示的輸出結果,可以看到三個程式集的版本全部變成了4.0.0.0,也就說真正被CLR載入的這些基礎程式集是與當前CLR的版本相匹配的。

2-12_thumb2

這個簡單的例項體現了這麼一個特徵:執行過程中載入的.NET Framework程式集(承載FCL的程式集)是由當前執行時(CLR)決定的,這些程式集的版本總是與CLR的版本相匹配。包含在元資料中的程式集資訊提供目標程式集的名稱,而版本則由當前執行的CLR來決定,我們將這個重要的機制稱為“程式集一致性(Assembly Unification)”,下圖很清晰地揭示了這個特性。

2-13png_thumb[3] 

Retargetable程式集

在預設情況下,如果某個程式集引用了另一個具有強簽名的程式集,CLR在執行的時候總是會根據程式集檔名、版本和公鑰令牌去定位目標程式集。如果無法找到一個與之完全匹配的程式集,一般情況下會丟擲一個FileNotFoundException型別的異常。如果當前引用的是一個Retargetable程式集,則意味著CLR在定位目標程式集的時候可以 “放寬” 匹配的要求,即指要求目標程式集具有相同的檔名即可。

如下圖所示,我們的應用程式(App)引用了具有強簽名的程式集“Foobar, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”,所以對於編譯後生成的程式集App.exe來說,對應的程式集引用將包含目標程式集的檔名、版本和公鑰令牌。如果在執行的時候只提供了一個有效名稱為“Foobar, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d7fg7asdf7asd7aer”的程式集,除了檔名,後者的版本號和公鑰令牌都與程式集引用元資料描述的都不一樣。在預設情況下,系統此時總是會丟擲一個FileNotFoundException型別的異常,倘若Foobar是一個Retargetable程式集,我們提供的將作為目標程式集被載入並使用。

2-14_thumb[2]

除了定義程式集的元資料多瞭如下一個retargetable標記之外,Retargetable程式集與普通程式集並沒有本質區別。

普通程式集:  

.assembly Foobar

Retargetable程式集: 

.assembly retargetable Foobar

這樣一個retargetable標記可以通過按照如下所示的方式在程式集上應用AssemblyFlagsAttribute特性來新增。不過這樣的重定向僅僅是針對.NET Framework自身提供的基礎程式集有效,雖然我們也可以通過使用AssemblyFlagsAttribute特性為自定義的程式集新增這樣一個retargetable標記,但是CLR並不會賦予它重定向的能力。

[assembly:AssemblyFlags(AssemblyNameFlags.Retargetable)] 

如果某個程式集引用了一個Retargetable程式集,自身清單檔案針對該程式集的引用元資料同樣具有如下所示的retargetable標記。CLR正式利用這個標記確定它引用的是否是一個Retargetable程式集,進而確定針對該程式集的載入策略,即採用針對檔名、版本和公鑰令牌的完全匹配策略,還是採用只針對檔名的降級匹配策略。

針對普通程式集的引用:

   1: 針對普通程式集的引用
   2: .assembly extern Foobar
   3: {
   4:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         
   5:   .ver 1:0:0:0
   6: }

針對Retargetable程式集的引用:

   1: .assembly extern retargetable Foobar
   2: {
   3:   .publickeytoken = (B7 7A 5C 56 19 34 E0 89)                         
   4:   .ver 1:0:0:0
   5: }

型別的轉移

在進行框架或者產品升級過程,我們經常會遇到針對程式集的合併和拆分的場景,比如在新版本中需要對現有的API進行從新規劃,可能會將定義在程式集A中定義的型別轉移到程式集B中。但是即使發生了這樣的情況,我們依然需要為新框架或者產品提供向後相容的能力,這就需要使用到所謂“型別轉移(Type Forwarding)”的特性。

為了讓讀者朋友們對型別轉移這個重要的特性具有一個大體的認識,我們來作一個簡單的例項演示。我們利用Visual Studio建立一個針對.NET Framework 3.5的控制檯應用App,並在作為程式入口的Main方法中編寫了如下兩行程式碼將兩個常用的型別(StringFunc<>)所在的程式集名打印出來。程式編譯之後會在 “\bin\Debug” 目錄下生成可執行檔案App.exe和對應的配置檔案App.exe.config。從如下給出的配置檔案內容可以看出.NET Framework 3.5採用的執行時(CLR)版本為 “v2.0.50727” 。

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

App.exe.config

   1: <configuration>
   2:   <startup>
   3:     <supportedRuntime&nbsp;version="v2.0.50727"/></startup>
   4:   </startup>
   5: </configuration>

現在我們直接以命令列的執行執行編譯生成的App.exe後會在控制檯上得到如下圖所示的輸出結果。可以看出對於我們給出的這兩個基礎型別(String和Func<>),只有String型別被定義在程式集mscorlib.dll之中,而型別Func<>其實被定義在另一個叫做System.Core.dll的程式集之中。其實Framework 2.0、3.0和3.5不僅僅共享相同的執行時(CLR 2.0),對於提供基礎型別的核心程式集mscorlib.dll也是共享的,下圖輸出的版本資訊已經說明了這一點。也就是說,.NET Framework 2.0釋出時提供的程式集mscorlib.dll在.NET Framework 3.x時代就沒有升級過。Func<>型別是在.NET Framework 3.5釋出時提供的一個基礎型別,所以不得不將它定義在一個另一個程式集中,微軟將這個程式集命令為System.Core.dll

2-15_thumb2 

現在我們看看.NET Framework 4.0(CLR 4.0)環境下運行同一個應用程式(App.exe)是否會有不同的輸出結果。為此我們在不對專案做重新編譯情況下直接修改配置檔案App.exe.config,並按照如下所示的方式將執行時版本設定為4.0。

   1: <configuration>
   2:   <startup>
   3:     <supportedRuntime&nbsp;version="v4.0"/>
   4:   </startup>
   5: </configuration>

下圖是同一個App.exe在.NET Framework 4.0環境下的輸出結果,可以看出我們提供的兩個基礎型別所在的程式集都是mscorlib.dll。也就是當.NET Framework升級到4.0之後,不僅僅執行時升級到了全新的CLR 4.0,微軟同時也對承載基礎型別的mscorelib.dll程式集進行了重新規劃,所以定義在System.Core.dll程式集中的基礎型別也基本上又重新回到了mscorlib.dll這個本應該屬於它的程式集中。

2-16_thumb2

我們來繼續分析上面演示的這個程式。由於App.exe這個程式集最初是針對目標框架.NET Framework 3.5編譯生成的,所以它的清單檔案將包含針對mscorlib.dll(2.0.0.0)和System.Core.dll(3.5.0.0)的程式集引用。下面的程式碼片段展示了針對這兩個程式集引用的元資料的定義。

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

當App.exe在.NET Framework 4.0環境中執行時,由於它的元資料提供的是針對System.Core.dll程式集的引用,所以CLR總是試圖載入該程式集並從中定位目標型別(比如我們演示例項中的型別Func<>)。如果當前執行環境無法提供這個程式集,那麼毫無疑問,一個FileNotFoundException型別的異常會被丟擲來。也就是,雖然型別Func<>在.NET Framework 4.0中已經轉移到了新的程式集mscorlib.dll中,當前環境依然會提供一個檔名為System.Core.dll的程式集。

System.Core.dll存在的目的是告訴CLR它需要載入的型別已經發生轉移,並將該型別所在的新的程式集名稱告訴它,那麼.NET Framework 4.0環境中的System.Core.dll是如何描述型別Func<>已經轉移到程式集mscorelib.dll之中了呢?如果分析程式集System.Core.dll中的元資料,我們可以看到如下一段於此相關的程式碼。在程式集的清單檔案中,每一個被轉移的型別都對應這個這麼一個 “.class extern forwarder” 指令。

   1: .class extern forwarder System.Func`1
   2: {
   3:   .assembly extern mscorlib
   4: }

不同於上面介紹的Retargetable程式集,型別的轉移並不是只針對.NET Framework提供的基礎程式集,如果我們自己開發的專案也需要提供類似的向後相容性,也可以使用這個特性。針對型別轉移型別的程式設計只涉及到一個型別為TypeForwardedToAttribute的特性,接下來我們通過一個簡單的例項來演示一下如何利用這個特性將某個型別轉移到一個新的程式集中。

我們利用Visual Studio建立瞭如下圖所示的解決方案,它演示了這樣一個場景:控制檯應用使用到了V1版本的類庫Lib(v1\Lib),其中涉及到一個核心型別Foobar。該類庫升級到V2版本時,我們選擇將所有的核心型別統一定義在新的程式集Lib.Core中,所以型別Foobar需要轉移到Lib.Core中。作為類庫的釋出者,我們希望使用到V1版本的應用能夠直接升級到V2版本,也就是升級的應用不需要在引用新的Lib.Core程式集情況下對原始碼進行重新編譯,而是直接部署V2版本的兩個程式集(Lib.dll和Lib.Core)就可以了。

2-17_thumb2

上圖中的虛線箭頭和實線箭頭分別代表專案之間的引用關係,我們從中可以看出v2目錄下的Lib專案具有對Lib.Core專案的引用,因為它需要引用轉移到Lib.Core專案中的型別。為了完成針對型別Foobar的轉移,我們只需要在v2\Lib中定義如下一行簡單的程式碼就可以了,我們將這行程式碼定義在AssemblyInfo.cs檔案中。

 [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Lib.Foobar))] 

為了檢驗針對Foobar型別的轉移是否成功,我們在控制檯應用App中定義瞭如下一段程式,它負責將Foobar型別當前所在程式集的名稱輸出到控制檯上。接下來我們只需要編譯(以Debug模式)整個解決方案,那麼V2版本的兩個程式集(Lib.dll和Lib.Core.dll)將儲存到\v2\lib\bin\debug\目錄下。

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

接下來我們採用命令列的形式來執行控制檯程式App.exe。如下圖所示,我們將當前目錄切換到App.exe所在的目錄(\app\bin\debug)下並執行App.exe,輸出的結果表明Foobar型別當前所在的程式集為Lib.dll。接下來我們將針對V2版本的兩個程式集拷貝進來後再次執行App.exe,我們發現此時的Foobar型別已經是從新的程式集Lib.Core.dll中載入的了。

2-18_thumb2

我們順便來檢視一下V2版本程式集Lib.dll的清單檔案的內容。如下面的程式碼片段所示,在原始碼中通過使用TypeForwardedToAttribute特性定義的型別轉移在編譯之後被轉換成了一個“.class extern forwarder”指令。

   1: .assembly extern Lib.Core
   2: {
   3:   .ver 1:0:0:0
   4: }
   5: .class extern forwarder Lib.Foobar
   6: {
   7:   .assembly extern Lib.Core
   8: }
   9:

三、可移植類庫(PCL)

在.NET Framework的時代,建立可移植類庫(PCL:Portable Class Library)是實現跨多個目標框架程式集共享的唯一途徑。上面介紹的內容都是在為PCL做鋪墊,只有充分理解了Retargetable程式集型別轉移的前提下才可能瞭解PCL的實現原理有正確的理解。考慮到很多讀者朋友並沒有使用PCL的經歷,所以我們先來介紹一下如何建立一個PCL專案。 當我們採用Visualization Studio的Class Library(Portal)專案模板建立一個PCL專案的時候,需要在如下圖所示的對話方塊中選擇支援的目標框架及其版本。Visual Studio會為新建的專案新增一個名為 “.NET” 的引用,這個引用指向一個由選定目標框架決定的程式集列表。由於這些程式集提供的API能夠相容所有選擇的平臺,我們在此基礎編寫的程式自然也具有平臺相容性。

2-20_thumb[2] 

如果檢視這個特殊的.NET引用所在的地址,我們會發現它指向目錄“%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable\{version}\Profile\ProfileX”。如果檢視 “%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable” 目錄,我們會發現它具有如下圖所示的結構。

2-21_thumb[2] 

如上圖所示,本機所在目錄“%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable”下具有三個代表.NET Framework版本的子目錄(v4.0、v4.5和v4.6)。具體到針對某個.NET Framework版本的目錄(比如v4.6),其子目錄Profile下具有一系列以 “Profile” + “數字” (比如Profile31、Profile32和Profile44等)命名的子目錄,實際上PCL專案引用的就是儲存在這些目錄下的程式集

對於兩個不同平臺的.NET Framework來說,它們的BCL在API的定義上存在交集,從理論上來說,建立在這個交集基礎上的程式是可以被這兩個平臺中共享的。如下圖所示,如果我們編寫的程式碼需要分別對Windows Desktop/Phone、Windows Phone/Store和Windows Store/Desktop平臺提供支援,那麼這樣的程式碼依賴的部分僅限於兩兩的交集A+B、A+C和A+D。如果要求這部分程式碼能夠執行在Windows Desktop/Phone/Store三個平臺上,那麼它們只能建立在三者之間的交集A上。  

2-22_thumb[1]

針對所有可能的目標框架(包括版本)的組合,微軟會將作為兩者交集的API提取出來並定義在相應的程式集中。比如說所有的目標框架都包含一個核心的程式集mscorlib.dll,雖然定義其中的型別及其成員在各個目標框架不盡相同,但是它們之間肯定存在交集,微軟針對不同的目標框架組合將這些交集提取出來並定義在一系列同名程式集中,並同樣命名為mscorlib.dll。 微軟按照這樣的方式建立了其他針對不同.NET Framework平臺組合的基礎程式集,這些針對某個組合的所有程式集構成一系列的Profile,並定義在上面我們提到過的目錄下。值得一提的是,所有這些針對某個Profile的程式集均為Retargetable程式集。

當我們建立一個PCL專案的時候,第一個必需的步驟是選擇相容的目標框架(和版本),Visual Studio會根據我們的選擇確定一個具體的Profile,併為建立的專案新增針對該Profile的程式集引用。由於所有引用的程式集是根據我們選擇的目標框架組合 “度身定製” 的,所以定義在PCL專案的程式碼才具有可移植的能力。

上面我們僅僅從開發的角度解釋了定義在PCL專案的程式碼本身為什麼能夠確保是與目標.NET Framework平臺相容的,但是在執行的角度來看這個問題,卻存在額外兩個問題:

  • 元資料描述的引用程式集與真實載入的程式集不一致,比如我們建立一個相容.NET Framework 4.5和Silverlight 5.0的PCL專案,被引用的程式集mscorlib.dll的版本為2.0.5.0,但是Silverlight 5.0執行時環境中的程式集mscorlib.dll的版本則為5.0.5.0
  • 元資料描述的引用程式集的型別定義與執行時載入程式集型別定義不一致,比如引用程式集中的某個型別被轉移到了另一個程式集中。

由於PCL專案在編譯時引用的均為Retargetable程式集,所以程式集的重定向機制幫助我們解決了第一個問題。因為在CLR在載入某個Retargetable程式集的時候,如果找不到一個與引用程式集在檔名、版本、語言文化和公鑰令牌完全匹配的程式集,則會只考慮檔名的一致性。至於第二個問題,自然可以通過上面我們介紹的型別轉移機制來解決。

綜上所述,雖然微軟在針對多個目標框架的程式碼複用上面為我們提供了一些解決方案。在原始碼共享方面,我們可以採用共享專案,雖然共享專案能夠做到將一組原始檔進行打包複用,但是我個人基本上不怎麼用它,因為如果我們在其中定義一些公有型別,那麼引用該共享專案的專案之間會造成命名衝突。從另一方面講,我們真正需要的是程式集層面的複用,但是在這方面微軟只為我們提供了PCL。PCL這種採用提取目標框架API交集的方式註定了只能是一種臨時的解決方案,試著想一下:如果目標框架由10種,每種有3個版本,我們需要為多少種組合建立相應的Profile。對於開發者來說,如果目標框架(包括版本),我們在建立PCL專案進行相容框架的選擇都會成問題。所以我們針對希望的是能夠提供給全平臺支援的BCL,你可以已經知道了,這就是Net Standard,那麼Net Standard是如何能夠在多個目標框架中複用的呢?請求關注本系列終結篇《.NET Core跨平臺的奧祕[下篇]:全新的佈局》。