天天看點

.NET Standard 來日苦短去日長

作者:Richard

翻譯:精緻碼農-王亮

原文:http://dwz.win/Q4h

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

概要

.NET 5 将是一個具有統一功能和 API 的單一産品,可用于 Windows 桌面應用程式、跨平台移動應用程式、控制台應用程式、雲服務和網站。

.NET Standard 來日苦短去日長

為了更好地說明這一點,我們更新了這篇[1]關于 TFM (Target Framework Names) 介紹的文章(譯文:.NET 5 中 Target Framework 詳解),現支援的 TFM 如下:

  • .net5.0

    ,表示代碼可在任意平台運作,它合并并替換了

    netcoreapp

    netstandard

    這兩個名稱。這個 TFM 通常隻包括跨平台的技術(除了一些為了滿足實用性而作出讓步的 API,就像我們在 .NET Standard 中所做的那樣)。
  • net5.0-windows

    (還有後面會增加的

    net6.0-android

    net6.0-ios

    ),這些 TFM 表示 .NET 5 特定于作業系統的風格,包含

    net5.0

    和特定于作業系統的功能。

我們不會再釋出 .NET Standard 的新版本,但是 .NET 5 和所有未來的版本将繼續支援 .NET Standard 2.1 和更早的版本。你應該将

net5.0

(和未來的版本)視為共享代碼的基礎。

由于

net5.0

是所有這些新 TFM 的共用的基礎,這意味着運作時、庫和新的語言特性都會圍繞這個版本号進行協調。例如,為了使用 C# 9,你需要使用

net5.0

net5.0-windows

如何選擇 Target

.NET 5 和所有未來的版本将繼續支援 .NET Standard 2.1 和更早的版本,從 .NET Standard 重新 Target 到 .NET 5 的唯一原因是為了獲得更多運作時特性、語言特性或 API 支援。是以,你可以把 .NET 5 想象成 .NET Standard 的 vNext。

那新代碼呢?該從 .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 上的使用者使用最新的平台特性。

幾年後,可重用庫的選擇将隻涉及

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%。

.NET Standard 來日苦短去日長

但是隻标準化 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 Standard 來日苦短去日長

如果不能實作真正意義上的合并,我們就無法解決這個問題,這正是 .NET 5 所做的:它提供了一個統一的實作,各方都建立在相同的基礎上,進而得到相同的 API 和版本号。

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

當我們設計 .NET Standard 時,為了避免過多地破壞庫的生态系統,我們不得不做出讓步[4]。也就是說,我們不得不包含一些 Windows 專用的 API(如檔案系統 ACL、系統資料庫、WMI 等)。今後,我們将避免在

net5.0

net6.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 版本。

.NET Standard 來日苦短去日長

我們的期望是,每次釋出(大版本)時,我們都會把所有架構名稱連在一起釋出。例如,它可能看起來像這樣:

.NET Standard 來日苦短去日長

這意味着你心裡可以有個預期,無論我們在 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],稱為

    browser

    browser-wasm

    。它們允許包的作者在浏覽器中針對 WebAssembly 部署不同的二進制檔案。這對于需要事先編譯成 WebAssembly 的本地代碼特别有用。

如上所述,我們已經标記了在浏覽器沙盒中不支援的 API,例如

System.Diagnostics.Process

。如果你從浏覽器應用内部使用這些 API,你會得到一個警告,告訴你這個 API 是不支援的。

總結

net5.0

是為能在任何平台運作的代碼而設計的,它結合并取代了

netcoreapp

netstandard

名稱。我們還有針對特定平台的架構,比如

net5.0-windows

(後面還有

net6.0-android

net6.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

作者:精緻碼農-王亮

出處:http://cnblogs.com/willick

聯系:[email protected]

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。如有問題或建議,請多多賜教,非常感謝。

繼續閱讀