引用程序集 是一种特殊类型的程序集,仅包含表示库的公共 API 图面所需的最小元数据量。 它们包括在生成工具中引用程序集时所需的所有成员的声明,但不包括所有成员实现以及对其 API 协定没有明显影响的私有成员的声明。 相比之下,常规程序集称为 实现程序集。
无法加载引用程序集以供执行,但它们可以像实现程序集一样作为编译器输入传递。 引用程序集通常与特定平台或库的软件开发工具包(SDK)一起分发。
使用引用程序集,开发人员可以生成面向特定库版本的程序,而无需该版本的完整实现程序集。 假设计算机上只有某些库的最新版本,但你想要生成面向该库早期版本的程序。 如果直接针对实现程序集进行编译,则可能无意中使用在早期版本中不可用的 API 成员。 只有在目标计算机上测试程序时,才会发现此错误。 如果针对早期版本的引用程序集进行编译,将立即收到编译时错误。
引用程序集还可以表示一个协定,即一组不对应于具体实现程序集的 API。 此类引用程序集(称为 协定程序集)可用于面向支持同一组 API 的多个平台。 例如,.NET Standard 提供协定程序集 netstandard.dll,该程序集表示在不同 .NET 平台之间共享的一组通用 API。 这些 API 的实现包含在不同平台上的不同程序集中,例如 .NET Framework 上的 mscorlib.dll 或 .NET Core 上的 System.Private.CoreLib.dll 。 面向 .NET Standard 的库可以在支持 .NET Standard 的所有平台上运行。
使用引用程序集
要在项目中使用某些 API,必须添加对其程序集的引用。 可以添加对实现程序集或引用程序集的任何一种引用。 建议在引用程序集可用时使用它。 这样做可确保仅使用目标版本中支持的 API 成员,这些成员将由 API 设计器使用。 使用参考程序集可确保不依赖于实现细节。
.NET Framework 库的引用程序集随目标包一起分发。 可以通过下载独立安装程序或在 Visual Studio 安装程序中选择组件来获取它们。 有关详细信息,请参阅 安装面向开发人员的 .NET Framework。 对于 .NET Core 和 .NET Standard,引用程序集将在必要时(通过 NuGet)进行自动下载和引用。 对于 .NET Core 3.0 及更高版本,核心框架的引用程序集位于 Microsoft.NETCore.App.Ref 包中( Microsoft.NETCore.App 包用于 3.0 之前的版本)。
使用 “添加引用 ”对话框在 Visual Studio 中添加对 .NET Framework 程序集的引用时,从列表中选择一个程序集,Visual Studio 会自动查找与项目中所选的目标框架版本对应的引用程序集。 这同样适用于使用 引用 项目项将引用直接添加到 MSBuild 项目中:只需指定程序集名称,而不是完整的文件路径。 使用 -reference
编译器选项(在 C# 和 Visual Basic 中)或使用 Compilation.AddReferences Roslyn API 中的方法在命令行中添加对这些程序集的引用时,必须为正确的目标平台版本手动指定引用程序集文件。 .NET Framework 引用程序集文件位于 %ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETFramework 目录。 对于 .NET Core,可以通过将项目属性设置为PreserveCompilationContext
,强制发布操作将目标平台的引用程序集复制到输出目录的 true
子目录中。 然后,可以将这些引用程序集文件传递给编译器。 使用 DependencyContext
Microsoft.Extensions.DependencyModel 包可以帮助查找这些路径。
由于它们不包含任何实现,因此无法加载引用程序集以供执行。 如果尝试这样做,则会导致 System.BadImageFormatException。 如果要检查引用程序集的内容,你可将其加载到 .NET Framework 中的仅反射上下文中(使用 Assembly.ReflectionOnlyLoad 方法),或者加载到 .NET 和 .NET Framework 中的 MetadataLoadContext。
生成引用程序集
当库使用者需要针对许多不同版本的库生成其程序时,为库生成引用程序集非常有用。 分发所有这些版本的实现程序集可能不切实际,因为它们的大小很大。 引用程序集的大小较小,并将它们作为库 SDK 的一部分进行分发会减少下载大小并节省磁盘空间。
IDE 和生成工具还可以利用引用程序集来减少包含多个类库的大型解决方案的生成时间。 通常,在增量生成方案中,当项目的任何输入文件发生更改时,将重新生成项目,包括它所依赖的程序集。 每当程序员更改任何成员的实现时,实现程序集将发生更改。 仅当引用程序集的公共 API 受到影响时才会更改。 因此,将引用程序集用作输入文件,而不是实现程序集允许在某些情况下跳过依赖项目的生成。
可以通过以下方式生成引用程序集:
- 在 MSBuild 项目中,使用
ProduceReferenceAssembly
项目属性。 - 从命令行编译程序时,通过指定
-refonly
(C# / Visual Basic ) 或-refout
(C# / Visual Basic) 编译器选项。 - 使用 Roslyn API 时,通过在传递给EmitOptions.EmitMetadataOnly方法的对象中,将
true
设置为EmitOptions.IncludePrivateMembers,false
设置为Compilation.Emit。
如果要使用 NuGet 包分发引用程序集,则必须将它们包含在包目录下的 ref\ 子目录中,而不是用于实现程序集的 lib\ 子目录中。
引用程序集结构
引用程序集是相关概念( 仅元数据程序集)的扩展。 仅包含元数据的程序集会将方法主体替换为一个 throw null
主体,但包括除匿名类型以外的所有成员。 使用 throw null
主体(而不是无主体)的原因是 PEVerify 可以运行和传递(从而验证元数据的完整性)。
引用程序集进一步从仅包含元数据的程序集中删除元数据(私有成员):
- 引用程序集仅包含 API 界面中所需的引用。 实际程序集可能具有与特定实现相关的其他引用。 例如,
class C { private void M() { dynamic d = 1; ... } }
的参考程序集不引用dynamic
所需的任何类型。 - 在删除私有函数成员(方法、属性和事件)不会对编译产生可观察影响的情况下,它们会被删除。 如果没有 InternalsVisibleTo 属性,也会删除内部函数成员。
引用程序集中的元数据继续保留以下信息:
- 所有类型,包括专用类型和嵌套类型。
- 所有属性(甚至是内部属性)。
- 所有虚拟方法。
- 显式接口实现。
- 显式实现的属性和事件,因为它们的访问器是虚拟的。
- 结构的所有字段。
引用程序集包括程序集级 ReferenceAssembly 属性。 可以在源中指定此属性;然后编译器不需要合成它。 由于此属性,运行时将拒绝加载引用程序集以供执行(但它们可以在仅反射模式下加载)。
确切的引用程序集结构详细信息取决于编译器版本。 如果确定这些元数据不影响公共 API 图面,则较新版本可能会选择排除更多元数据。
注释
本节中的信息仅适用于从 C# 版本 7.1 或 Visual Basic 版本 15.3 开始的 Roslyn 编译器生成的程序集。 .NET Framework 和 .NET Core 库的引用程序集的结构可能会有所不同,因为它们使用自己的生成引用程序集的机制。 例如,它们可能具有完全为空的方法主体,而不是使用 throw null
正文。 但一般原则仍然适用:它们没有可用的方法实现,并且仅包含从公共 API 的角度来看具有可观察影响的成员的元数据。