【译】介绍 .NET Standard
https://zhuanlan.zhihu.com/p/24267356 跟开发争执过 自己不会写代码 的确不好。
若有任何对翻译的建议,烦请指正
有任何问题?请查阅 .NET Standard FAQ。
在我上一篇博文中,我曾提到我们想要使迁移到 .NET Core 更容易。在这篇博文中,我将专注于我们是怎样使用 .NET Standard 以实现这个计划的。我们将会涉及到诸如哪些API将被包含,跨平台兼容性是怎样工作的,以及这一切对于 .NET Core 意味着什么等内容。
如果你对细节感兴趣,那么这篇文章很适合你。但如果你没空或对细节不感兴趣,别担心:你可以只看“长话短说”这一节。
给赶时间的人:长话短说
.NET Standard 通过引入在桌面应用,移动应用和游戏以及云服务等不同环境中 .NET 开发者所期待和喜爱的 API 以解决跨平台的代码共享问题:
- .NET Standard 是一组所有 .NET 平台都应实现的 API。这将统一 .NET 平台,并预防将来的碎片化。
- .NET Standard 2.0 将会被 .NET Framework,.NET Core,和 Xamarin 所实现。对于 .NET Core 来说,将会新增许多被要求加入的(.NET Framework)现有的API。
- .NET Standard 2.0 包含一个 .NET Framework 二进制文件的兼容层,可以显著增加 .NET Standard 库可以引用的库的数量。
- .NET Standard 将替代可移植类库(PCLs) 成为跨平台 .NET 库的开发工具。
- 你可以在 Github 的仓库 dotnet/standard 上看到 .NET Standard API的定义。
为什么我们需要一个标准?
在博文介绍 .NET Core 中我们曾详细介绍过,.NET 平台在过去几年中产生了许多分支。一方面来说,这实际上是件好事。它使你可以裁剪 .NET 以满足单一的平台所不能满足的需求。例如,.NET Compact Framework 就是为了满足 2000 年前后资源受限的手机的需求而生的。在今天,(这种做法)同样适用:Unity (Mono 的一个分支)在超过 20 个平台上运行。能够创建新分支和可定制性是任何技术都需要的一项重要能力。
但是在另一方面来说,这种行为为开发人员编写跨 .NET 平台的代码带来了巨大的麻烦,因为没有统一的类库可用:
![](https://i-blog.csdnimg.cn/blog_migrate/e0e06b94d1f72088de4f121c598ac56b.png)
目前主要有三种版本的 .NET,这意味着你需要掌握三种不同的基础类库以写出可以在三种平台上运行的代码。由于业界与 .NET 刚建立之时相比更加的多样化,可以想象我们的工作将随着新的 .NET 平台的创建一直进行下去。无论是 Microsoft 还是任何(有这类想法的)人,都可以构建一个新的 .NET 版本以支持新的操作系统或是去裁剪它以适应特定设备的配置。
这是 .NET Standard 将要处在的位置:
![](https://i-blog.csdnimg.cn/blog_migrate/d7353b30db96e069168ce5142a3ee189.png)
对于开发者而言,这意味着他们只需要掌握一种基础类库。以 .NET Standard 作为目标平台的类库将可以在所有 .NET 平台上运行。同时,平台提供者也无需猜测为了使用 NuGet 上的库,他们需要提供哪些 API 。
应用程序。在应用程序这个层次,你不需要直接使用 .NET Standard。然而,你仍将间接受益。首先,.NET Standard 保证所有的 .NET 平台使用同样功能的基础类库。如果你学会了如何在桌面应用中使用它,你同时也学会了如何在移动应用和云服务中使用它。其次,使用 .NET Standard 将使大多数类库可以被用在任何地方,这意味着基层的一致性也将扩散至更大的 .NET 生态系统。
可移植类库。让我们来和现在的可移植类库的运作方式做个比较。使用可移植类库,你选择你想发布的平台,然后工具将展示所有你可用的 API。所以尽管工具帮助你使类库运行在不同平台,它仍然迫使你去考虑不同的基础类库。而 .NET Standard 只有一个基础类库。它的全部内容都将被所有 .NET 平台支持——无论是现在还是将来。另一个重要方面,API 的可用性是非常可预测的:版本越高,API 越多。而对可移植类库来说,这不是必然的:可用的 API 集合是所选平台的 API 集合的交集,这通常会生成一个你无法轻易预料的 API 层。
API 的连贯性。 如果你对比 .NET Framework,.NET Core,和 Xamarin/Mono,你会注意到 .NET Core 提供了最小规模的 API 层(不包含 OS 相关的 API)。首要的不一致之处就是基础 API 可用性的重大差异(例如网络和加解密的 API)。第二个 .NET Core 导致的问题是核心部分 API 的不一致,尤其是反射。这两个不一致之处是导致向 .NET Core 迁移的难度高于预期的主要原因。通过建立 .NET Standard,我们正在编写在所有 .NET 平台拥有统一的 API 的要求,这同时包括了API 的可用性和一致性(方面的要求)。
版本和工具。 正如我在介绍 .NET Core 一文中所提到的,我们对于 .NET Core 的目标,是为一个可统一 API 和实现,可移植的 .NET 平台打下基础。我们打算让其成为可移植类库的下一个版本。不幸的是,它的工具使用体验并不是很好。由于我们的目标是代表任意一个 .NET 平台,我们将它拆成了更小的 NuGet 包。如果这些组件都能随着应用程序被部署,则它们表现的非常好,因为你可以单独的更新这些组件。然而,当你指定了一个抽象规范,例如可移植类库或 .NET Standard,这件事就没那么美好了,因为为了可以运行在正确的平台集上,需要非常具体的版本组合。为了避免这种情况,我们将 .NET Standard 定义为一个单独的 NuGet 包。由于它只表示所需的一组 API,(我们)不需要再去将它们拆分,因为所有的 .NET 平台都必须原生的支持这个(包)。唯一重要的维度是它的版本,和 API 等级类似:版本越高,API 越多,而版本越低,则越多 .NET 平台实现了它。
总结一下,我们需要 .NET Standard 有两个理由:
- 对一致性的动力。 我们想达成一个共识,所有 .NET 平台都必须实现一组必需的 API 才可以获得进入 .NET 库生态系统的权限。
- 优秀的跨平台工具的基础。 我们希望一个简化的工具体验,你只需要选择一个版本号来指定所有 .NET 平台的通用部分。
.NET Standard 2.0 有哪些更新?
当我们发布 .NET Core 1.0 时,我们同时介绍了 .NET Standard。.NET Standard 有许多版本,以表示这套 API 在现有的各个平台上的可用性。下表展示了哪些现有平台的版本与指定 .NET Standard 版本相兼容:
![](https://i-blog.csdnimg.cn/blog_migrate/d21c72dde3d8c53189e9eb6159cdceb3.png)
箭头表示此平台支持更高等级的 .NET Standard。例如,.NET Core 1.0 支持 1.6 版的 .NET Standard,这就是 1.0 版 到 1.5 版是箭头的原因。
你可以借助这张表来分析你的目标平台可用的最高版本的 .NET Standard 是哪个版本。例如,如果你想在 .NET Framework 4.5 和 .NET Core 1.0 上运行,你最高只能将目标平台设为 .NET Standard 1.1。
你也可以了解哪些平台将支持 .NET Standard 2.0:
- 我们即将发布新增了所有支持 .NET Standard 2.0 所必需API的 .NET Core,Xamarin和 UWP 的一个升级版本。
- .NET Framework 4.6.1 已经实现了所有属于 .NET Standard 2.0 的 API。请注意,这个版本出现了两次;我会在稍后解释为什么以及它是如何运作的。
.NET Standard 同时也兼容可移植类库。从可移植类库的配置到 .NET Standard 版本的映射已在我们的文档中列出。
在一个目标平台为 .NET Standard 的库中你将可以引用两种类型的库:
- .NET Standard,如果库的版本低于或等于你的目标版本。
- 可移植类库,如果库的配置文件可以被映射到一个低于或等于你指定的 .NET Standard 版本。
如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/7740f5ae000135ed8ab93c9bddbd4565.png)
不幸的是,NuGet 上对可移植类库和 .NET Standard 的接受程度并不是很高,以至于没有达到可以顺畅使用他们的地步。下面是指定目标平台在 http://NuGet.org 上的包中出现的次数:
![](https://i-blog.csdnimg.cn/blog_migrate/6bda6d8286512435a3c0ee6fdd7c5c03.png)
如你所见,显然大部分 NuGet 上的类库以 .NET Framework 作为目标平台。然而,我们了解到这些库中有很大一部分只使用了我们将在 .NET Standard 2.0 中列出的 API。
在 .NET Standard 2.0中,我们将使在 .NET Standard 库中通过一个兼容层去引用已有的 .NET Framework 的库成为可能:
![](https://i-blog.csdnimg.cn/blog_migrate/be2dd43fad033a6251cd7ba11e3dc8fd.png)
当然,这只对那些只使用了适用于 .NET Standard API 的 .NET Framework 库有效。这也是为什么不推荐以这种方式构建跨 .NET 平台库的理由。然而,这个兼容层也为你将自己引用了尚未转换的库的库转换成 .NET Standard 库提供了一座桥梁。
如果你想了解更多关于这个兼容层是怎样工作的,请查阅 .NET Standard 2.0 的规范。
.NET Standard 2.0 中的重大更新:增加对 .NET Framework 4.6.1 的兼容
一个标准只有在有平台实现了它之后才有用。我们想同时让 .NET Standard 本身变得有意义和有用,因为它是适用于那些以标准作为目标平台的库的 API 层:
- .NET Framework. .NET Framework 4.6.1 有最高的接受度,这使它成为最引人注意的目标 .NET Framework 版本。因此,我们想保证它可以实现 .NET Standard 2.0 。
- .NET Core. 正如上面所提到的,.NET Core 有一个比 .NET Framework 或 Xamarin 都小得多的 API 集。(使它)支持 .NET Standard 2.0 意味着我们要显著扩展它的 API 范围。由于 .NET Core 并不是随操作系统而是随应用发行的,只需要更新 SDK 和我们的 NuGet 包就可以支持 .NET Standard 2.0。
- Xamarin. Xamarin 已经支持大部分 .NET Standard 的 API。(它的)更新将类似于 .NET Core——我们希望更新 Xamarin 以补全所有目前缺失的 API。事实上,大部分缺失的 API 已经在 the stable Cycle 8 release/Mono 4.6.0 中被添加了。
之前列出的表格展示了哪个版本的 .NET Framework 支持哪个版本的 .NET Standard:
![](https://i-blog.csdnimg.cn/blog_migrate/9000913b59efbe09f8988780759a448e.png)
按照通常的版本规则,可以预期 .NET Standard 2.0 将只会被更高的 .NET Framework 版本所支持,目前最新的 .NET Framework 版本(4.6.2)只支持 .NET Standard 1.5。这意味着在 .NET Standard 2.0 之前编译的库将不能在大部分(包含) .NET Framework 设备上运行。
为了使 .NET Framework 4.6.1 支持 .NET Standard 2.0,我们必须将所有 .NET Standard 1.5 和 1.6 引入的 API 移除。
你可能会好奇这个决定所造成的影响是什么。我们对所有 http://NuGet.org 上的包进行了一次分析,观察它们是否以 .NET Standard 1.5 或更高为目标版本并调用了这些 API。在写这篇文章时,我们只发现了 6 个非 Microsoft 的包这样做了。我们将联系这些包所有者并与他们一同解决这个问题。通过检查它们的使用方式,(我们发现)显然这些调用可以被 .NET Standard 2.0 中引入的 API 所取代。
为了让这些包的作者支持 .NET Standard 1.5,1.6 和 2.0,他们需要明确的指定这些版本并交叉编译。另外,他们可以选择指定 .NET Standard 2.0 和更高版本,(这将)提供一个广泛的支持的平台集。
.NET Standard 有哪些内容?
我们使用以下几个步骤来决定哪些 API 将成为 .NET Standard 的一部分:
- 输入。 我们从 .NET Framework 和 Xamarin 都有的 API 开始入手。
- 评估。 我们把这些 API 分成以下两类:
1. 必需的。 对于那些我们希望所有平台都提供且我们认为可以被跨平台实现的 API,我们标记为 必需的。
2. 可选的。 对于那些平台相关的或是遗留技术,我们标记为 可选的。
可选的 API 将不会成为 .NET Standard 的一部分,但将以独立的 NuGet 包的形式提供。我们尽可能的将 .NET Standard 作为这些库的目标平台以使它们可以在任意平台上使用,但对平台相关的 API 来说不一定行得通(例如 Windows 注册表)。
为了使一些 API 成为可选的,我们可能需要移除部分必需的 API。例如,我们判定 AppDomian 应该在 .NET Standard 中而代码访问安全性(CAS)是遗留组件。为此我们需要移除 AppDomain 中所有使用了 CAS 的类型的成员,例如 CreateDomain 的接受证据的重载方法。
.NET Standard 的 API 集以及我们的可选 API 目标都可以在 .NET Standard’s review body 处审阅。
这是 .NET Standard 2.0 API 层的高度概括:
![](https://i-blog.csdnimg.cn/blog_migrate/680b8704c391ec29e178e304ac74f17c.png)
我还能使用平台相关的 API 吗?
在建立适用于多平台类库的经验时遇到的最大挑战之一就是避免只拥有最不通用的分母(共通处),同时还要确保你不会意外创建比你预期的更加难以移植的库。
在可移植类库中我们通过拥有不同的、代表一系列平台的(API 的)交集的配置文件解决了这个问题。好处是你可以尽可能的在一组目标平台间扩展你的 API 集。.NET Standard 表示一组所有 .NET 平台都必须实现的 API。
这带来了关于我们是如何为无法在所有平台上实现的 API 建模的问题:
- 运行时相关的 API。 例如,在运行时通过反射生成和运行代码的能力。这项特性无法在没有 JIT 编译器的 .NET 平台上工作,例如 UWP 的 .NET Native 或 Xamarin 的 iOS 工具链。
- 操作系统相关的 API。 在 .NET 中我们封装了许多 Win32 API 以便于使用。正如 Windows 注册表。(它们的)实现依赖于底层的 Win32 API,而在别的平台上没有等价物。
关于这些 API,我们有一些选项:
- 使这些 API 失效。 你无法使用那些不能跨 .NET 平台运行的 API。
- 保持这些 API 可用,但抛出 PlatformNotSupportedException。 这意味着无论这些 API 是否被所有平台支持,我们都将其暴露出来。不支持这些 API 的平台提供它们,但是(调用时)会抛出 PlatformNotSupportedException。
- 模拟这些 API。 Mono 在 .ini 文件的基础上实现注册表。尽管对于那些使用注册表读取系统信息的应用不起作用,它仍在应用程序只是简单的储存自身的状态和用户设置的情景下工作的很好。
我们相信最好的选项是将它们结合起来。就如我们之前提到过的,我们希望 .NET Standard 代表一组所有 .NET 平台都必须实现的 API。我们希望使这组 API 易于实现,同时确保受欢迎的 API 存在以使编写跨平台的库轻松和直观。
我们处理只在部分 .NET 平台上可用的技术的通常的做法是将它们做成基于 .NET Standard 的 NuGet 包。因此如果你创建了一个基于 .NET Standard 的库,默认情况下它不会引用这些 API。你需要添加 NuGet 包来引入它们。
这种做法对于那些独立的的 API 来说很有效,因此(它们)可以被放进独立的包中。对于那些类型中个别成员无法在所有平台实现的情况,我们将使用第二和第三种方法:平台必须有这些成员,但它们可以决定抛出(异常)或是模拟它们。
让我们看看一些例子,以及我们是打算如何处理它们的:
- 注册表。 Windows 注册表是一个以独立 NuGet 包形式提供的独立的的组件(例如 Microsoft.Win32.Registry)。你可以在 .NET Core 中使用它,但它只在 Windows 上有作用。在其他平台调用注册表的 API 将引发 PlatformNotSupportedException。你应该正确的保护你的调用,或是保证只会在 Windows 上运行。我们正在考虑改善我们的工具以帮助你检测这类问题。
- 应用程序域。 AppDomain 类型有许多不与创建应用程序域紧密相关的 API,例如列出所有被加载的程序集,或是注册一个未处理异常的处理函数。这些 API 在 .NET 库生态系统中经常被用到。基于这种情况,我们觉得最好将这个类型加入到 .NET Standard并使少部分与应用程序域的创建相关的 API 在不支持的平台上抛出异常,例如 .NET Core。
- 反射动态生成。 反射动态生成大致上独立,因此我们计划按照注册表的思路来处理它。其他的一些 API (是否可用)取决于是否能供动态生成代码,例如表达式树的 Compile 方法,或是编译正则表达式的功能。在部分情况下,我们将模拟这些行为(例如解释执行而不是编译表达式树),其他情况下我们将抛出异常(例如编译正则表达式的时候)。
一般来说,你可以像现在一样指定特定的 .NET 平台来绕过这些在 .NET Standard 上不可用的 API。我们正在考虑改善我们的工具,以使从平台相关到平台无关的迁移更加顺畅,这样一来你们可以选择最适合你们的情景的选项,而不会被早期的设计选项所困扰。
总而言之:
- 我们会公布不一定在所有 .NET 平台上可用的想法。
- 我们一般会将它们做成需要你们显式引用的独立的包。
- 在极少数情况中,特殊的成员可能会抛出异常。
(我们的)目标是使基于 .NET Standard 的库尽可能的强大和有丰富的表达力,同时保证在你依赖不一定在所有地方可用的技术时提示你。
这对 .NET Core 意味着什么?
我们精心设计了 .NET Core 以使它的引用程序集成为可移植的 .NET 库的范例(PS:此段翻译不一定准确。如有更好的翻译,欢迎指正)。这使得添加新的 API 变得困难,因为向 .NET Core 添加这些 API (的行为)提前决定了它们是否该在所有平台上可用。更坏的情形是,由于版本规则的限制,这也意味着我们需要决定哪些 API 组合该以怎样的顺序可用。
单独分发。 我们试着通过将这些 API 做成基于现有 API 的新组件来使它们可被单独分发。对于很容易这样做的技术,这将是首选的方法,因为这也意味着任何 .NET 开发者可以与这些 API 打交道并向我们提供反馈。我们已经在不可变集合上(用这种方法)取得了巨大的成功。
推断运行时特性。 然而,对于那些需要运行时支持的特性来说会很困难,因为我们不能仅仅给你一个可用的 NuGet 包。我们也需要提供一种获得更新过的运行时的方法。这在拥有系统级运行时的平台上比较困难(例如 .NET Framework),在一般的平台上也很困难,因为我们有多种适用于不同目的的运行时(例如 JIT 和 AOT)。一次性引入所有这些方面是不现实的。.NET Core 的好处是这个平台被设计成完全独立的。因此在未来,我们更有可能利用这种特性进行实验和预览。
从 .NET Core 中分离出 .NET Standard。 为了能独立于其他 .NET 平台来发展 .NET Core,我们将 .NET Core 的可移植机制(我之前提到的那个)剥离了出来。.NET Standard 被定义为一个独立的、适用于所有 .NET 平台的引用程序集。每个 .NET 平台使用一组不同的引用程序集,因此它们可以随心所欲的添加新的 API。基于此,我们可以决定其中的哪些会被加入 .NET Standard 并变得通用。
从 .NET Core 分离出可移植机制帮助我们加速 .NET Core 的开发,也使新特性的实验变得更加简单。我们可以简单的修改那些需要被修改以支持新特性的层,而不是别扭的尝试去基于现有的平台来设计特性。我们也可以将 API 添加到它们逻辑上所属的类,而不用去担心这些类是否已被移植到了其他平台上。
在 .NET Core 中添加新的 API 并不是它们是否将会进入 .NET Standard 的声明,我们对于 .NET Standard 的目标是建立并维持 .NET 平台间的一致性。因此已经是标准的一部分的类的新成员将自动在标准更新的时候被考虑。
作为一名库作者,我现在应该做什么?
作为一名库作者,你应该考虑迁移到 .NET Standard,因为它将在目标为多个平台的情景下取代可移植类库。
在 .NET Standard 1.x 中可用的 API 集和可移植类库非常相似。但 .NET Standard 2.x 将有一个显著变大的 API 集,同时也允许你依赖以 .NET Framework 为目标平台的库。
可移植类库和 .NET Standard 的关键区别有:
- 平台约束。 可移植类库面临的一大问题就是你指定多个平台时,它仍然是一组特定的 API。尤其对 NuGet 包来说更是如此,因为你必须在库文件夹名中列出所有平台,例如 portable-net45+win8。在支持相同 API 的新的平台出现时,这种情况会导致问题。.NET Standard 不存在这种问题,因为你只是指定了一个不包含任何平台信息的标准版本,例如 netstandard1.4.
- 平台可用性。 可移植类库目前支持很多平台,而并不是每一个(平台的)配置文件都有一个匹配的 .NET Standard 版本。查阅此文档以查看更多细节。
- 库可用性。 可移植类库被设计成不允许依赖目标平台无法运行的 API 和库(的样子)。因此,可移植类库仅允许你引用指定了你的可移植类库所指定的平台的超集的可移植类库。.NET Standard 也类似,但它额外允许你引用目前 .NET 生态系统事实上的硬通货——.NET Framework 的库。因此,在 .NET Standard 2.0 中你将有一个更大的库的集合。
为了做出明智的决定,我建议你:
- 使用 API 移植来检查你的代码库与不同 .NET Standard 版本的兼容性如何。
- 通过查阅 .NET Standard 的文档来保证你可以在对你来说较为关键的平台上运行。
例如,如果你想知道你是否需要等 .NET Standard 2.0 (的发布),你可以下载 API 移植命令行工具并以如下方式来将你的库与 .NET Standard 1.6 和 .NET Standard 2.0 对比:
apiport analyze -f C:\src\mylibs\ -t ".NET Standard,Version=1.6"^
-t ".NET Standard,Version=2.0"
注意: .NET Standard 2.0 是一项正在进行的工作,因此 API 将可能会变动。同时我也提醒你们注意那些在 .NET Standard 1.6 中存在但在 .NET Standard 2.0 中被移除的那些 API。
总结
我们已经建立了 .NET Standard,因此不同 .NET 平台间代码的共享和复用将变得更加简单。
在 .NET Standard 2.0 中,我们正专注于兼容性。为了在 .NET Core 和 UWP 中支持 .NET Standard 2.0,我们将扩展这些平台以包含更多的现有 API。这也包括了一个允许你引用为 .NET Framework 编译的库的兼容层。
在未来,我们建议你使用 .NET Standard 而不是可移植类库。以 .NET Standard 2.0 为目标的工具将与代号为“Dev 15“的 Visual Studio 一同发布。你将以 NuGet 包的形式引用 .NET Standard。它将同时在 Visual Studio,VS Code 和 Xamarin Studio 中获得最优先的支持。
你可以通过关注我们全新的Github 代码仓库 dotnet/standard 来关注我们的进展。
请让我们知道你的想法!