.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

相关文章
相关标签/搜索