.NET Standard 来日苦短去日长

dotNET全栈开发

共 11559字,需浏览 24分钟

 ·

2020-11-06 00:35

作者:Richard

翻译:精致码农-王亮

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

自从 .NET 5 宣贯以来,很多人都在问这对 .NET Standard 意味着什么,它是否仍然重要。在这篇文章中,我将解释 .NET 5 是如何改进代码共用并取代 .NET Standard 的,我还将介绍什么情况下你仍然需要 .NET Standard。

概要

.NET 5 将是一个具有统一功能和 API 的单一产品,可用于 Windows 桌面应用程序、跨平台移动应用程序、控制台应用程序、云服务和网站。

为了更好地说明这一点,我们更新了这篇[1]关于 TFM (Target Framework Names) 介绍的文章(译文:.NET 5 中 Target Framework 详解),现支持的 TFM 如下:

  • .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(和未来的版本)视为共享代码的基础。

由于 net5.0 是所有这些新 TFM 的共用的基础,这意味着运行时、库和新的语言特性都会围绕这个版本号进行协调。例如,为了使用 C# 9,你需要使用 net5.0net5.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%。

但是只标准化 API 就会产生额外的付出,它要求我们在添加新 API 时进行协调——这一直在发生。.NET 开源社区(包括.NET 团队)通过提供新的语言特性、可用性改进、新的交叉(cross-cutting)功能(如 Span)或支持新的数据格式或网络协议,不断对 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)。对所有的功能都这样做实在是太昂贵了,这也是为什么有不少功能还是错过了 .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 的警告。你必须在项目文件中添加 项,明确表示你打算在 Blazor WebAssembly 中支持您的项目:

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

  <PropertyGroup>
    <TargetFramework>net5.0TargetFramework>
  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
回复 【关闭】广
回复 【实战】获取20套实战源码
回复 【被删】
回复 【访客】访
回复 【小程序】学获取15套【入门+实战+赚钱】小程序源码
回复 【python】学微获取全套0基础Python知识手册
回复 【2019】获取2019 .NET 开发者峰会资料PPT
回复 【加群】加入dotnet微信交流群

终于GitHub App 已支持简体中文!


微信钱包“免费提现”的方法来了!


浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报