再來多聊一點 .Net Standard

  • 7119
  • 0
  • 2017-11-09

大約在 11 個月前,我寫了一篇文章簡介 ASP.NET Core 與 Net Standard 之間的關係 , 後來黑暗執行緒也記錄了一篇與 Net Standard 2.0 有關的文章.從前面這兩個文章,可以讓你在概念上大概知道什麼是 Net Standard.隨著 VS 2017 即將上市,相關的工具鍵都較為完備的情況下,現在就比較適合再深入了解 Net Standard 的內容了.

如果你的專案是以前的 PCL或有計畫將現有的 .Net Framework 程式轉成 .Net Core,則這篇文章適合你來看.如果你的專案需要用 .Net 技術來實現跨 OS,則這篇文章也很適合你來看.

工具鏈的改變

在去年之前,當你寫上 .Net Core 程式,總是會看到 project.json.其實在微軟的開發工具中,它並不是主流,幾乎所有的開發工具產品都用不上它.當時為了要跨 OS (因為當時 msbuild 沒有跨 OS),所以就做出了一個新產物,然後當你執行 dotnet build 時,便使用這個新產物在不同 OS 的情況下來進行 build 的動作.所以,project.json 就是那個時候所產生的一個組態設定檔.在 project.json 裡,可以看到專案能支援的 framework 以及 runtime,也包含其他的資訊,如 reference, tools 等等.在那個時候,Net Standard 當然也是其中的一個 framework 選項之一,你可以把你的專案建置為 Net Standard 元件.只不過那個時候 Net Standard 還在剛起步的階段,許多微軟內部的工程師也不太清楚這是什麼東西,包括當時的我也是一樣.後來,隨著 msbuild 有 .Net Core 與 Mono 的版本之後,project.json 的未來就被終結了,畢竟它是新東西,絕大部份的工具鍵仍都是採用 msbuild,所以這個新東西就很自然地消失.但 dotnet.exe 工具並沒有消失,只是變成了 msbuild 的入口程式.所以,當你用新版的 .Net Core CLI 在執行 dotnet build 時,你就會發現出現的工具是 msbuild.MSBuild 看不懂 project.json,因此 project.sjon 的消失而轉入 csproj 的格式,也是可以預見的情況,就看 PM 大老闆們何時做出這項決定了.這項改變對 .Net Core 與 ASP.NET 相關團隊影響蠻大的,因為有不少的工具鏈都要隨之而改.儘管如此,這樣的改變也許比較好吧,因為就不用懂兩套 build 的工具了,只要專心處理 msbuild 即可..Net Standard 在本質上雖然跟上述的事件沒有直接關係,但是在與其他團隊配合上時,也會因此受到時間上的影響.所以,到去年下半年後才能看到有比較完整的工具鏈支持著 .Net Standard 在 Visual Studio 上的操作.

.Net Standard 與 PCL (Portable Class Library) 

在理論上來說,.Net Standard 可以說是新一代的 PCL.因為他們是出自同一個工程團隊.但這兩者之間還是存在著一些想法上的差別.

PCL 感覺起來是一個事後為了彌補某些事情而產生出來的做法.為什麼這麼說呢 ? 因為相關的 platform 產生之後,才有 PCL 的產生.比如,你今天因為工作上的需要,需要寫一個元件能夠同時支援 .Net Framework 和 UWP.由於,這是兩個不同的 framework,他們所包含的 API 也不盡相同.為了要製做出一個能共同支援這兩個平台的元件,那只好取兩邊的 API 聯集.我們把這個稱為 PCL 一號.改天如果又出現了另一個 framework,也因為支援平台的數量不同時,就會出現不一樣的 API 聯集,因此就會產生類似像 PCL 一號,二號,三號等等這樣的情況發生.真實上編號並不是依序編的,在這只是為了說明簡單而舉的例子.在下圖來說明.

來源: https://www.youtube.com/ImmoLandwerth

如上圖所示,假設你有 3 個 platform,每一個 platform 的 API 之間不盡相同,有的一樣,有的不一樣.因此就會產生 4 塊不同的 intersections.如果你今天要製做一個元件同時支援 Platform 1 and Platform 3,那麼你的 API intersection 一定是箭頭最上方的那一塊.如果要同時支援三個 platform,需要的 API intersection 便是中間的那一塊.每一個 intersection 就會給定一個編號,於是編號固定下來之後,就可以用簡單的查表法來得知這個元件所同時支援的 platform 是什麼.PCL 指的就是這些 API intersection 裡製做出來的 class library.因為可以同時支援多個 platform,所以才會取名叫 "portable" class library.假設今天再多出一個 Platform 4 的話,你也不難想像 PCL 的編號會怎樣地發展下去.到這裡,你就可以感覺到為何我說 PCL 就好像是為了彌補某些事情而產生出來的做法.光是看到 PCL 的編號,我想很難會有人記得這編號到底是同時支援了那些 platform.

.Net Standard 的製做想法跟 PCL 不同..Net Standard 先規範出了一些 API intersection 內容,把不同的 intersection 定義出不同的版本編號,然後再看每個 platform 能支援到什麼版本.

來源: https://github.com/dotnet/standard/blob/master/docs/versions.md

到目前為止,.Net standard 定義了 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.0 等版本.每個版本所定義好的 API intersection 的內容是不一樣的.在 1.0 版所定義出的 API intersection 最少.版本越大,intersection API 數量越多,因為你可以看到越大的版本所支援的 platform 漸少.舉個例子說明如何看懂上面這張圖.假設你今天要為客戶做一個元件同時支援 .Net Core 與 .Net Framework 和 Xamarin.Android.而你的客戶規定 .Net Framework 只能用 .Net Framework 4.6 以上,因此你能夠選最低的 .Net standard 版本是 1.3,因為 Net standard 1.3 所定義的 API intersection 可在 .Net Core 1.0 以上, .Net Framework 4.6 以上,Mono 4.6 以上, Xamarin.iOS 10.0 以上, Xamarin.Android 7.0 以上, UWP 10.0 以上來使用.如果你選擇 .Net Standard 1.2 版,這樣就會出現問題了,因為 .Net standard 1.2 所定義的 API intersection 中是 .Net Framework 4.5.1 版,換句話說,它包含不到 .Net Framework 4.6 裡新定義的 API.你可以到上圖中的網址去看這張表格,表格上的 .Net Standard 編號都有網頁可以告訴你到底有那些 API 定義在該版本裡.同時,網頁也提供了 api 與前一版本的數量差異與清單.

當每個 platform 一直發展下去後,你就可以感覺到有種他們是跟著 .Net Standard 所定義的規範在走.比如說,假設 .Net Standard 2.1 版包含了 .Net Framework 4.7 版,2.1 版裡面多定義了一個 API 叫 abc,那就表示 .Net framework 4.7 版裡面有 abc API,如果 .Net Core 2.0 版被定義在 .Net standard 2.1 時,那就表示 .Net Core 2.0 也一定會有 abc API,其他的 platform 也得依照辦理.如果你曾在某些投影片裡面看到介紹 .Net standard 時 (如下圖),底下會有一句話 One library to rule them all,就是這種感覺.當時我看到這張投影片,我笑了,想必做這投影片的 PM 一定是個 Lord of the Rings 的小說迷.

來源: https://www.youtube.com/ImmoLandwerth

看到目前為止,希望你能感受到 .Net Standard 並不算是一個實做任何東西的 platform,其實它是一系列的規範來要求每個 platform 在不同的版本下需要實做出那些 API..Net Standard 1.6 到 .Net Standard 2.0 有著相當大的差別,也就是說 API 數量多了很多.從這網址上你可以看到 .Net Standard 2.0 在每個 namespace 所增加出來的 API 數量.相信未來的 .Net Standard 2.0 將可以滿足許多人現在的 API 需求.

把你的程式 Port 到 .Net Core ?

當 .Net Core 在去年釋出後,我便觀察到在一些 open source 的專案裡,有許多的 open source project 紛紛支援了 .Net Core platform.例如,非常受大家歡迎的 NewtonSoft.JSON,早期它支援 .Net framework,而它也繼續支援 .Net Core,所以不論你寫 .Net framework 還是 .Net Core 的程式,都可以用到它.如果你的公司也有計畫做跨 OS 的支援,藉由 .Net Core 的能力將公司的元件可以延伸到 Linux, Mac 等,那麼移植到 .Net Core 的確是個好主意.但是,在看過了上述的介紹之後,你只想要移植到 .Net Core 而己嗎 ? 如果你能將你的元件移植成 .Net standard 元件,那代表你能支援的 platform 種類將更多.例如,你現有一個以 .Net framework 4.5 為基礎來開發的元件,如果你把它移植成 .Net standard 1.1 的元件,那表示你能支援的 platform 不止是 .Net Core 1.0,還包含 Mono 4.6, Xamarin.iOS 10, Xamarin.Android 7, UWP 10, Windows 8.0, 以及 Windows Phone 8.1.透過 .Net standard,你的元件能支援的 platform 更多了,實在沒理由不移植到 .Net Standard 來試一試呀.換句話說,在合理的情況下,你所製做的元件都儘量做成 .Net Standard 元件,這樣才能支援更多的 platform.

Type Forwarding

前面曾提到 .Net Standard 只是一系列的規範,本身並不包含任何的平台實做內容.比如,.Net Standard 定義了一個 WriteFile API,所以真正在做寫檔案動作的人可不是 .Net Standard,而是你執行時的 platform 裡面一定會有個 WriteFile API 來運作的.接下來就說明這些運作內容..Net Stanard 專案裡面會有一個 NuGet Package (NetStandard.Library),而裡面會有一個 netstandard.dll,就會由它來做為溝通的管道,而搭建這個管道的方法就稱為 Type Fowarding.例如,當你的程式使用了一個 .Net Standard 元件,在這元件裡面呼叫了 .Net Standard 所定義的 WriteFile API,然後 WriteFile API 是 platform API,所以 netstandard.dll 就會依照你所執行的 platform 然後去找到 WriteFile,真正的實做程式碼是在這裡,所以 netstandard.dll 就會呼叫它來執行,因此你的程式就可以執行 WriteFile 動作了.

因此,在程式 build 的時候,.Net standard 就可以把元件裡面的 API 和 platform API 溝通管道架設起來,而在 runtime 的時候,platform API 的實作內容就給可提供給 netstandard.dll 來執行.用下圖來說明:

來源: https://www.youtube.com/ImmoLandwerth

假設你的 .Net standard 元件正使用 .Net Framework (設定為Target framework),而你的元件裡所定義的 API 都是在用 .Net Framework 裡面的 API.相信你也知道 .Net Framework 的程式都是透過 mscorlib.dll 來執行的,所以它所建立起來的 Object 一定和 netstandard.dll 所建立起來的 Object 不一樣 (type mismatch),因此會有一個 Type Forwarding 的關鍵技術在說明 mscorlib.dll 的什麼物件就是 netstandard.dll 中的什麼物件.以上是 build time 發生的事情.

來源: https://www.youtube.com/ImmoLandwerth

當你的程式執行時 (run time),你的程式裡參考了你的 .Net Standard 元件,也參考了其他 .Net Framework 的元件,而當你的程式執行到 .Net Standard 元件裡的內容時,這時 netstandard.dll 就會依照在 build time 所得到的對應資料,然後就直接把要執行的動作交給所對應的 API 去執行.透過這樣的 Type forwarding,netstandard.dll 就能去找當下 platform 的 API 實作內容來執行所需要的程式碼.

VS 2017 上的支援

以工具鏈的角度來看,在 VS 2017 剛釋出時,只會提供到 .Net Standard 1.6 的內容..Net Standard 2.0 "應該" 是在今年夏天和 .Net Core 2.0 一起推出.(不敢保證,所以只說"應該").到時推出之後,相信 VS2017 的相關工具鏈也會一併更新 (應該是在 15.2 更新,也差不多是今年六七月的時間點)

如果你已在用 VS 2017,你已經可以在專案範本中看到 .Net Standard 範本.

.Net Standard 程式範例

接下來,用 VS 2017 為 .Net Standard 元件做一點簡單的範例.首先,開啟一個 .Net Standard 專案,並且再開始一個 .Net Core Console 專案和一個 .Net Framework Console 專案.完成之後,整個 solution 結構如下:

特別把 .Net Standard 元件的 Dependencies 打開,讓你可以看到 NETStandard.Library (netstandard.dll) 在裡面.在 .Net Standard 元件專案裡,把  Class1.cs 改寫成如下:

    public class NetStandardClass
    {
        public string GetString()
        {
            return "say hello from .net standard!";
        }
    }

然後再將 .Net Standard 元件 reference 到另外兩個 Console 專案.然後在這兩個 Console 專案去使用 NetStandardClass 並且呼叫 GetString(),你會得到如下結果:

你可以看到這兩個不同 platform 的 Console 都可以使用這個 .Net Standard 元件.

前面提到 project.json 已經消失了,所以在 .Net Standard 專案裡已經看不到 project.json.在以前 project.json 的時間下,我們可以把專案設定多個 framework 做為執行的目標.現在改成 .csproj 之後,這當然也可以做到,你可以打開專案的 .csproj,然後把 TargetFramework 更改如下:  (請注意, TargetFrameworks 是複數,多一個 s)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard1.4;net461;netcoreapp1.0</TargetFrameworks>
  </PropertyGroup>

</Project>

這表示這個 .Net Standard 元件會做出三種版本的元件,分別是執行在 .Net Standard 1.4, .Net framework 4.6.1, 以及 .Net Core 1.0 下.因此當你 build 這個 .Net Standard 元件專案時,你在 bin folder 之下可以看到三份產出物.

這就是 multiple target framework 的結果.每個 folder 裡都有屬於該 platform 下的專用元件.同時在回到元件的 Dependencies 時,你可以發現 .Net Core 加進來了.

因為已經設定了 multiple target framework,如果你想針對每個 platform 執行特別的程式碼時,可以把 Class1.css 改成如下.

    public class NetStandardClass
    {
        public string GetString()
        {
#if NETCOREAPP1_0
                    return "Say hello to Net Core from net standard";
#elif NET461
                    return "Say hello to Net FX from net standard";
#else
            return "Say hello to no one";
#endif
        }
    }

接著再執行這兩個 Console 專案

你可以看到,當有 multiple target framework 時,你可以為特定的 platform 執行特定的動作.這個簡單的程式碼只是告訴你 .Net standard 元件有這樣的特性,不是鼓勵你程式要這樣寫.如果你直接在一大串的程式裡面加一堆 #if #else #endif ,我想你的老闆或同事可能會敲你腦袋.建議大家,若有需要針對特定 platform 執行特定動作時,請把它們做成不同的 class 封裝起來,然後 #if #endif 這類的宣告式只需要寫在 Factory class,因為在 Factory class 裡,只需要 create class 然後 return 出去,因此 #if #endif 這樣的語法便不會造成困擾.好的寫法很多種,只是不希望造成大家誤會直接把 #if #endif 寫在一堆 code 裡面.

若你需要知道更多的細節,請參考 https://www.youtube.com/ImmoLandwerth,主講者是 .Net Standard 團隊的 PM.

書面資料在 https://github.com/dotnet/standard/tree/master/docs

Hope it helps,