.NET Standard 來日苦短去日長

做者:Richard
翻譯:精緻碼農-王亮
原文:http://dwz.win/Q4handroid

自從 .NET 5 宣貫以來,不少人都在問這對 .NET Standard 意味着什麼,它是否仍然重要。在這篇文章中,我將解釋 .NET 5 是如何改進代碼共用並取代 .NET Standard 的,我還將介紹什麼狀況下你仍然須要 .NET Standard。ios

概要

.NET 5 將是一個具備統一功能和 API 的單一產品,可用於 Windows 桌面應用程序、跨平臺移動應用程序、控制檯應用程序、雲服務和網站。git

爲了更好地說明這一點,咱們更新了這篇[1]關於 TFM (Target Framework Names) 介紹的文章(譯文:.NET 5 中 Target Framework 詳解),現支持的 TFM 以下:github

  • .net5.0,表示代碼可在任意平臺運行,它合併並替換了 netcoreappnetstandard 這兩個名稱。這個 TFM 一般只包括跨平臺的技術(除了一些爲了知足實用性而做出讓步的 API,就像咱們在 .NET Standard 中所作的那樣)。
  • net5.0-windows(還有後面會增長的net6.0-androidnet6.0-ios),這些 TFM 表示 .NET 5 特定於操做系統的風格,包含 net5.0 和特定於操做系統的功能。

咱們不會再發布 .NET Standard 的新版本,可是 .NET 5 和全部將來的版本將繼續支持 .NET Standard 2.1 和更早的版本。你應該將 net5.0(和將來的版本)視爲共享代碼的基礎。web

因爲 net5.0 是全部這些新 TFM 的共用的基礎,這意味着運行時、庫和新的語言特性都會圍繞這個版本號進行協調。例如,爲了使用 C# 9,你須要使用 net5.0net5.0-windowswindows

如何選擇 Target

.NET 5 和全部將來的版本將繼續支持 .NET Standard 2.1 和更早的版本,從 .NET Standard 從新 Target 到 .NET 5 的惟一緣由是爲了得到更多運行時特性、語言特性或 API 支持。因此,你能夠把 .NET 5 想象成 .NET Standard 的 vNext。api

那新代碼呢?該從 .NET Standard 2.0 開始仍是直接從 .NET 5 開始?這得視狀況而定。瀏覽器

  • 應用程序組件,若是你要將你的應用程序以類庫的形式分解成多個組件,我建議將 netX.Y 做爲 TFM,netX.Y 中的 X.Y 是應用程序(或多個應用程序)的 .NET 最低版本號。爲了簡單起見,你可能但願全部組成你的應用程序的 Project 都使用相同的 .NET 版本,由於這樣能夠保證各處的代碼均可以使用相同的 BCL 特性。
  • 可重用庫,若是你正在構建計劃在 NuGet 上發佈的可重用庫,你將須要考慮適用範圍和可用新特性之間的權衡。.NET Standard 2.0 是 .NET Framework 支持的最高 .NET Standard 版本,因此它能夠知足你的大部分使用場景。咱們一般建議不要將 Target 鎖定在 .NET Standard 1.x 上,由於不值得再爲此增添沒必要要的麻煩。若是你不須要支持 .NET Framwork,那麼你能夠選擇 .NET Standard 2.1 或者 .NET 5,大多數代碼可能能夠跳過 .NET Standard 2.1 直接轉到 .NET 5。

那麼,你應該怎麼作呢?個人建議是,已被普遍使用的庫可能須要同時提供 .NET Standard 2.0 和 .NET 5 支持。支持 .NET Standard 2.0 將使你的庫適用性更廣,而支持 .NET 5 則確保你能夠爲已經在 .NET 5 上的用戶使用最新的平臺特性。bash

幾年後,可重用庫的選擇將只涉及 netX.Y 版本,這基本上是構建 .NET 庫的一慣作法——你一般要支持一段時間較老的版本,以確保沒有升級最新 .NET 版本的用戶依然可使用你的庫。網絡

總結一下:

  • 在 .NET Framework 和全部其餘平臺之間共享代碼,使用 netstandard2.0
  • 在 Mono、Xamarin 和 .NET Core 3.x 之間共享代碼,使用 netstandard2.1
  • 日後的共享代碼,使用 net5.0

.NET 5 如何解決 .NET Standard 存在的問題

.NET Standard 使得建立適用於全部 .NET 平臺的庫變得更加容易,可是 .NET Standard 仍然存在三個問題:

  1. 它的版本更新很慢[2],這意味着你不能輕鬆地使用最新的特性。
  2. 它須要一個解碼環[3]來將版本映射到 .NET 實現。
  3. 它公開了特定於平臺的特性[4],這意味着你不能靜態地驗證代碼是否真正可移植。

讓咱們看看 .NET 5 將如何解決這三個問題。

問題 1:.NET Standard 版本更新慢

在設計 .NET Standard[5] 時,.NET 平臺尚未在實現層次上融合,這使得編寫須要在不一樣環境下工做的代碼變得困難,由於不一樣的工做代碼使用的是不一樣的 .NET 實現。

.NET Standard 的目標是統一基礎類庫(BCL)的特性集,這樣你就能夠編寫一個能夠在任何地方運行的單一庫。這爲咱們提供了很好的服務:前 1000 個軟件包中有超過 77% 支持 .NET Standard。若是咱們看看 NuGet.org 上全部在過去 6 個月裏更新過的軟件包,採用率是 58%。

可是隻標準化 API 就會產生額外的付出,它要求咱們在添加新 API 時進行協調——這一直在發生。.NET 開源社區(包括.NET 團隊)經過提供新的語言特性、可用性改進、新的交叉(cross-cutting)功能(如 Span<T>)或支持新的數據格式或網絡協議,不斷對 BCL 進行創新。

而咱們雖然能夠以 NuGet 包的形式提供新的類型,但不能以這種方式在現有類型上提供新的 API。因此,從通常意義上講,BCL 的創新須要發佈新版本的 .NET 標準。

在 .NET Standard 2.0 以前,這並非一個真正的問題,由於咱們只對現有的 API 進行標準化。但在 .NET Standard 2.1 中,咱們對全新的 API 進行了標準化,這也是咱們看到至關多摩擦的地方。

這種摩擦從何而來?

.NET 標準是一個 API 集,全部的.NET 實現都必須支持,因此它有一個編輯方面[6]的問題,全部的 API 必須由 .NET Standard 審查委員會[7]審查。該委員會由 .NET 平臺實現者以及 .NET 社區的表明組成。其目標是隻對咱們可以真正在全部當前和將來的 .NET 平臺中實現的 API 進行標準化。這些審查是必要的,由於 .NET 協議棧有不一樣的實現,有不一樣的限制。

咱們預測到了這種類型的摩擦,這就是爲何咱們很早就說過,.NET 標準將只對至少一個 .NET 實現中已經推出的 API 進行標準化。這乍一看彷佛很合理,但隨後你就會意識到,.NET Standard 不可能頻繁地更新。因此,若是一個功能錯過了某個特定的版本,你可能要等上幾年才能使用,甚至可能要等更久,直到這個版本的 .NET Standard 獲得普遍支持。

咱們以爲對於某些特性來講,機會損失太大,因此咱們作了一些不天然的行爲,將尚未推出的 API 標準化(好比 AsyncEnumerable<T>)。對全部的功能都這樣作實在是太昂貴了,這也是爲何有很多功能仍是錯過了 .NET Standard 2.1 這趟列車的緣由(好比新的硬件特性)。

但若是有一個單一的代碼庫呢?若是這個代碼庫必須支持全部與 .NET 至今所實現功能有所不一樣的特性,好比同時支持及時編譯(JIT)和超前編譯(AOT)呢?

與其在過後才進行這些審查,不如從一開始就將全部這些方面做爲功能設計的一部分。在這樣的世界裏,標準化的 API 集從構造上來講,就是通用的 API 集。當一個功能實現後,由於代碼庫是共享的,因此你們就已經可使用了。

問題 2:.NET Standard 須要解碼環

將 API 集與它的實現分離,不只僅是減緩了 API 的可用性,這也意味着咱們須要將 .NET Standard 版本映射到它們的實現上[3]。做爲一個長期以來不得不向許多人解釋這個表格的人,我已經意識到這個看似簡單的想法是多麼複雜。咱們已經盡力讓它變得更簡單,但最終,這種複雜性是與生俱來的,由於 API 集和實現是獨立發佈的。

咱們統一了 .NET 平臺,在它們下面又增長了一個合成平臺,表明了通用的 API 集。從很現實的意義上來講,這幅漫畫是很到位的表達了這個痛點:

若是不能實現真正意義上的合併,咱們就沒法解決這個問題,這正是 .NET 5 所作的:它提供了一個統一的實現,各方都創建在相同的基礎上,從而獲得相同的 API 和版本號。

問題 3:.NET Standard 公開了特定平臺 API

當咱們設計 .NET Standard 時,爲了不過多地破壞庫的生態系統,咱們不得不作出讓步[4]。也就是說,咱們不得不包含一些 Windows 專用的 API(如文件系統 ACL、註冊表、WMI 等)。從此,咱們將避免在 net5.0net6.0 和將來的版本中加入特定平臺的 API。然而,咱們不可能預測將來。例如,咱們最近爲 Blazor WebAssembly 增長了一個新的 .NET 運行環境,在這個環境中,一些本來跨平臺的 API(如線程或進程控制)沒法在瀏覽器的沙箱中獲得支持。

不少人抱怨說,這類 API 感受就像「地雷」--代碼編譯時沒有錯誤,所以看起來能夠移植到任何平臺上,但當運行在一個沒有給定 API 實現的平臺上時,就會出現運行時錯誤。

從 .NET 5 開始,咱們將提供隨 SDK 發佈的默認開啓的分析器和代碼修復器。它包含平臺兼容性分析器,能夠檢測無心中使用了目標平臺並不支持的 API。這個功能取代了 Microsoft.DotNet.Analyzers.Compatibility NuGet 包。

讓咱們先來看看 Windows 特有的 API。

處理 Windows 特定 API

當你建立一個 Target 爲 net5.0 爲目標的項目時,你能夠引用 Microsoft.Win32.Registry 包。但當你開始使用它時:

private static string GetLoggingDirectory()
{
    using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Fabrikam"))
    {
        if (key?.GetValue("LoggingDirectoryPath") is string configuredPath)
            return configuredPath;
    }

    string exePath = Process.GetCurrentProcess().MainModule.FileName;
    string folder = Path.GetDirectoryName(exePath);
    return Path.Combine(folder, "Logging");
}

你會獲得如下警告:

CA1416: 'RegistryKey.OpenSubKey(string)' is supported on 'windows'
CA1416: 'Registry.CurrentUser' is supported on 'windows'
CA1416: 'RegistryKey.GetValue(string?)' is supported on 'windows'

你有三個選擇來處理這些警告。

  1. 調用保護:在調用 API 以前,你可使用 OperatingSystem.IsWindows() 來檢查當前運行環境是不是 Windows 系統。

  2. 將調用標記爲 Windows 專用:在某些狀況下,經過 [SupportedOSPlatform("windows")] 將調用成員標記爲特定平臺也有必定的意義。

  3. 刪除代碼:通常來講,這不是你想要的,由於這意味着當你的代碼被 Windows 用戶使用時,你會失去保真度(fidelity)。但對於存在跨平臺替代方案的狀況,你應該儘量使用跨平臺方案,而不是平臺特定的 API。例如,你可使用一個 XML 配置文件來代替使用註冊表。

  4. 抑制警告:固然,你能夠經過 .editorconfig#pragma warning disable 來抑制警告。然而,當使用特定平臺的 API 時,你應該更喜歡選項 (1) 和 (2)。

爲了調用保護,可使用 System.OperatingSystem 類上的新靜態方法,示例:

private static string GetLoggingDirectory()
{
    if (OperatingSystem.IsWindows())
    {
        using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Fabrikam"))
        {
            if (key?.GetValue("LoggingDirectoryPath") is string configuredPath)
                return configuredPath;
        }
    }

    string exePath = Process.GetCurrentProcess().MainModule.FileName;
    string folder = Path.GetDirectoryName(exePath);
    return Path.Combine(folder, "Logging");
}

要將你的代碼標記爲 Windows 專用,請應用新的 SupportedOSPlatform 屬性:

[SupportedOSPlatform("windows")]
private static string GetLoggingDirectory()
{
    using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Fabrikam"))
    {
        if (key?.GetValue("LoggingDirectoryPath") is string configuredPath)
            return configuredPath;
    }

    string exePath = Process.GetCurrentProcess().MainModule.FileName;
    string folder = Path.GetDirectoryName(exePath);
    return Path.Combine(folder, "Logging");
}

在這兩種狀況下,使用註冊表的警告都會消失。

關鍵的區別在於,在第二個例子中,分析器如今會對 GetLoggingDirectory() 的調用發出警告,由於它如今被認爲是 Windows 特有的 API。換句話說,你把平臺檢查的要求轉給調用者放去作了。

[SupportedOSPlatform] 屬性能夠應用於成員、類型和程序集級別。這個屬性也被 BCL 自己使用,例如,程序集 Microsoft.Win32.Registry 就應用了這個屬性,這也是分析器最早就知道註冊表是 Windows 特定 API 方法的緣由。

請注意,若是你的目標是 net5.0-windows,這個屬性會自動應用到你的程序集中。這意味着使用 net5.0-windows 的 Windows 專用 API 永遠不會產生任何警告,由於你的整個程序集被認爲是 Windows 專用的。

處理 Blazor WebAssembly 不支持的 API

Blazor WebAssembly 項目在瀏覽器沙盒內運行,這限制了你可使用的 API。例如,雖然線程和進程建立都是跨平臺的 API,但咱們沒法讓這些 API 在 Blazor WebAssembly 中工做,它們會拋出 PlatformNotSupportedException。咱們已經用 [UnsupportedOSPlatform("browser")] 標記了這些 API。

假設你將 GetLoggingDirectory() 方法複製並粘貼到 Blazor WebAssembly 應用程序中:

private static string GetLoggingDirectory()
{
    //...

    string exePath = Process.GetCurrentProcess().MainModule.FileName;
    string folder = Path.GetDirectoryName(exePath);
    return Path.Combine(folder, "Logging");
}

你將獲得如下警告:

CA1416 'Process.GetCurrentProcess()' is unsupported on 'browser'
CA1416 'Process.MainModule' is unsupported on 'browser'

你能夠用與 Windows 特定 API 基本相同的作法來處理這些警告。

你能夠對調用進行保護:

private static string GetLoggingDirectory()
{
    //...

    if (!OperatingSystem.IsBrowser())
    {
        string exePath = Process.GetCurrentProcess().MainModule.FileName;
        string folder = Path.GetDirectoryName(exePath);
        return Path.Combine(folder, "Logging");
    }
    else
    {
        return string.Empty;
    }
}

或者你能夠將該成員標記爲不被 Blazor WebAssembly 支持:

[UnsupportedOSPlatform("browser")]
private static string GetLoggingDirectory()
{
    //...

    string exePath = Process.GetCurrentProcess().MainModule.FileName;
    string folder = Path.GetDirectoryName(exePath);
    return Path.Combine(folder, "Logging");
}

因爲瀏覽器沙盒的限制性至關大,因此並非全部的類庫和 NuGet 包都能在 Blazor WebAssembly 中運行。此外,絕大多數的庫也不該該支持在 Blazor WebAssembly 中運行。

這就是爲何針對 net5.0 的普通類庫不會看到不支持 Blazor WebAssembly API 的警告。你必須在項目文件中添加 <SupportedPlatform> 項,明確表示你打算在 Blazor WebAssembly 中支持您的項目:

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

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>

</Project>

若是你正在構建一個 Blazor WebAssembly 應用程序,你沒必要這樣作,由於 Microsoft.NET.Sdk.BlazorWebAssembly SDK 會自動作到這一點。

.NET 5 是 .NET Standard 和 .NET Core 的結合

.NET 5 及後續版本將是一個單一的代碼庫,支持桌面應用、移動應用、雲服務、網站以及將來的任何 .NET 運行環境。

你可能會想「等等,這聽起來很不錯,但若是有人想建立一個全新的實現呢」。這也是能夠的。但幾乎沒有人會從頭開始一個新的實現。最有可能的是,它將是當前代碼庫(dotnet/runtime[8])的一個分支。例如,Tizen(三星智能家電平臺)使用的是 .NET Core,只作了細小的改動,並在上面使用了三星特有的應用模型。

Fork 保留了合併關係,這使得維護者能夠不斷從 dotnet/runtime[8] 倉庫中拉取新的變化,在不受其變化影響的領域受益於 BCL 創新,這和 Linux 發行版的工做方式很是類似。

固然,在某些狀況下,人們可能但願建立一個很是不一樣的「種類」的 .NET,好比一個沒有當前 BCL 的最小運行時。但這意味着它不能利用現有的 .NET 庫生態系統,它也不會實現 .NET Standard。咱們通常對這個方向的追求不感興趣,但 .NET Standard 和 .NET Core 的結合並不妨礙這一點,也不會增長難度。

.NET 版本

做爲一個庫做者,你可能想知道 .NET 5 何時能獲得普遍支持。從此,咱們將在每一年的 11 月發佈 .NET 新版本,每隔一年發佈一次長期支持(LTS)版本。

.NET 5 將在 2020 年 11 月正式發佈,而 .NET 6 將在 2021 年 11 月做爲 LTS 發佈。咱們建立了這個固定的時間表,使你更容易規劃您的更新(若是你是應用程序開發人員),並預測對支持的 .NET 版本的需求(若是你是庫開發人員)。

得益於 .NET Core 的並行安裝(譯註:一臺機器可同時安裝多個 .NET Core 版本,且向下兼容),它的新版本被採用速度至關快,其中 LTS 版本最受歡迎。事實上,.NET Core 3.1 是有史以來採用最快的 .NET 版本。

咱們的指望是,每次發佈(大版本)時,咱們都會把全部框架名稱連在一塊兒發佈。例如,它可能看起來像這樣:

這意味着你內心能夠有個預期,不管咱們在 BCL 中作了什麼創新,你都能在全部的應用模型中使用它,不管它們運行在哪一個平臺上。這也意味着,只要你運行最新版本的庫,你老是能夠在全部的應用模型消費最新的 net 框架帶來的庫。

這種模式消除了圍繞 .NET Standard 版本的複雜性,由於每次咱們發佈時,你均可以假設全部的平臺都會當即和徹底支持新版本,而咱們經過使用前綴命名慣例來鞏固這一承諾。

.NET 的新版本可能會添加對其餘平臺的支持。例如,咱們將經過 .NET 6 增長對 Android 和 iOS 的支持。相反,咱們可能會中止支持那些再也不相關的平臺。這一點能夠經過在 .NET 6 中不存在的 net5.0-someoldos 目標框架來講明。咱們目前沒有放棄一個平臺支持的計劃,那將是一個大問題,這不是預期的,如有咱們會提早好久宣佈。這也是咱們對 .NET Standard 的模式,例如,沒有新版本的 Windows Phone 實現了後面的 .NET Standard 版本。

爲何沒有 WebAssembly 的 TFM

咱們最初考慮爲 WebAssembly 添加 TFM,如 net5.0-wasm。後來咱們決定不這麼作,緣由以下:

  • WebAssembly 更像是一個指令集(如 x86 或 x64),而不是像一個操做系統,並且咱們通常不提供不一樣架構之間有分歧的 API。

  • WebAssembly 在瀏覽器沙箱中的執行模型是一個關鍵的差別化,但咱們決定只將其建模爲運行時檢查更有意義。相似於你對 Windows 和 Linux 的檢查方式,你可使用 OperatingSystem 類型。因爲與指令集無關,因此該方法被稱爲 IsBrowser() 而不是 IsWebAssembly()

  • WebAssembly 有運行時標識符(RID)[9],稱爲 browserbrowser-wasm。它們容許包的做者在瀏覽器中針對 WebAssembly 部署不一樣的二進制文件。這對於須要事先編譯成 WebAssembly 的本地代碼特別有用。

如上所述,咱們已經標記了在瀏覽器沙盒中不支持的 API,例如 System.Diagnostics.Process。若是你從瀏覽器應用內部使用這些 API,你會獲得一個警告,告訴你這個 API 是不支持的。

總結

net5.0 是爲能在任何平臺運行的代碼而設計的,它結合並取代了 netcoreappnetstandard 名稱。咱們還有針對特定平臺的框架,好比 net5.0-windows(後面還有 net6.0-androidnet6.0-ios)。

因爲標準和它的實現之間沒有區別,你將可以比使用 .NET Standard 更快地利用新功能。並且因爲命名慣例,你將可以很容易地知道誰可使用一個給定的庫--而無需查閱 .NET Standard 版本表。

雖然 .NET Standard 2.1 將是 .NET Standard 的最後一個版本,但 .NET 5 和全部將來的版本將繼續支持.NET Standard 2.1 和更早的版本。你應該將 net5.0(以及將來的版本)視爲將來共享代碼的基礎。

祝,編碼愉快!


文中相關連接:

[1].https://github.com/dotnet/designs/blob/master/accepted/2020/net5/net5.md
[2].https://github.com/dotnet/standard/tree/master/docs/governance#process
[3].https://dotnet.microsoft.com/platform/dotnet-standard#versions
[4].https://github.com/dotnet/standard/blob/master/docs/faq.md#why-do-you-include-apis-that-dont-work-everywhere
[5].https://devblogs.microsoft.com/dotnet/introducing-net-standard/
[6].https://github.com/dotnet/standard/tree/master/docs/governance#process
[7].https://github.com/dotnet/standard/blob/master/docs/governance/board.md
[8].https://github.com/dotnet/runtime
[9].https://docs.microsoft.com/en-us/dotnet/core/rid-catalog

相關文章
相關標籤/搜索