但是只标准化 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.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"); }