.NET 中的引用程序集
Intro
在 .NET 里有一种特殊的程序集叫做 ReferenceAssembly(引用程序集),引用程序集(Reference Assemblies) 是一种特殊类型的程序集,它只包含表示库的公共 API 所需的最少元数据量。它们包括在生成工具中引用程序集时所需的所有成员的声明,但不包括所有成员实现以及对其 API 协定没有明显影响的私有成员的声明。相比较下,常规程序集称为“实现程序集” (implementation assemblies)。
Why
既然我们有实现程序集,为什么还要有引用程序集呢?
使用引用程序集,开发人员可以生成面向特定库版本的程序,而无需具有该版本的完整实现程序集。因为不包含实现,引用程序集会更小一些,加载和解析都会更快一些。
这类似于我们和第三方的开发者约定的 API 规范,我们可以先给出 API 的请求和响应而无需提供实现以不 block 第三方开发者的进度,毕竟他们只关心 API 是什么样的而不关心实现。
若要使用项目中的某些 API,必须添加对其程序集的引用。可以将引用添加到实现程序集,也可以将其添加到引用程序集。建议在引用程序集可用时使用它。这样做可确保仅使用目标版本中受支持的 API 成员,即供 API 设计人员使用。使用引用程序集可确保不依赖于实现详细信息。
How
在 .NET Core 3.0 之前很多程序集都是发布 NuGet 包的,对于 .NET Core 3.0 和更高版本,核心框架的引用程序集位于 Microsoft.NETCore.App.Ref 包中,一般情况下是不需要的,因为引用程序集也会随着 .NET SDK 一起发布,你可以在 SDK 的安装目录下的 packs
目录下找到对应框架版本的引用程序集
下面是我电脑上 SDK 里的框架引用程序集的一个示例
对于引用程序集只能用于编译,这种程序集会有一些特殊,反编译的话会看到有一个 ReferenceAssembly 的程序集 Attribute,下面是我从上面的目录中找的 System.Text.Json
的反编译结果,可以看到有一个 ReferenceAssembly
的 attribute
再看一下 JsonNode
的实现
我们再找一个实现的程序集对比一下
由于它们不包含任何实现,因此无法加载引用程序集用于执行。如果尝试这样做,则会导致 System.BadImageFormatException,可能会遇到 Reference assemblies can only be loaded in the Reflection-only loader context. 这样的错误。
如果要检查引用程序集的内容,你可将其加载到 .NET Framework 中的仅反射上下文中(使用 Assembly.ReflectionOnlyLoad 方法),或者加载到 .NET Core 中的 MetadataLoadContext。
More
经常看源码的童鞋,一定会注意到,dotnet/runtime 中很多的类库的结构都是类似下面这样的
大家会看到第一个目录是 ref
,也就是用来生成引用程序集的,src
则是包含了实现的项目源码,test
则是一些测试用例 https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Microsoft.Extensions.Configuration/
ref
项目引用的其他项目也都是直接引用的 ref
项目 https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.csproj
查看 ref 项目的代码,可以发现和反编译的效果是一样的,都是空实现或者 throw null https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.cs#L7
最近在做 dotnet-exec 这个小工具的时候就遇到了引用程序集的问题,起初没怎么理解这个引用程序集,在编译代码时使用的是引用程序集,在执行代码时也是用的引用程序集,在执行时 load 程序集的时候就报了前面提到的
BadImageException
Reference assemblies can only be loaded in the Reflection-only loader context.
在看到 Youtube 上这个介绍 Reference Assembly 的视频(https://www.youtube.com/watch?v=EBpY1UMHDY8&list=PLRAdsfhKI4OX1cBGL2IXuEq1yzpDyKlwf&index=1&t=3s)之后才恍然大悟,原来如此。。。虽然视频是以 .NET Framework 为例讲解的,.NET Core 也类似,感兴趣的可以看一下
在 VS 里经常会遇到 F12 之后看到的实现都是 throw null
,猜测也是因为这个原因,在编译时 VS 使用的是引用程序集来提高性能
最后有没有好奇 ref 项目和 src 项目的差别在哪里?表面上看 ref 项目文件里的东西好像没什么特别的啊,利用了之前我们提到过的 Directory.Build.props 来为大多数项目统一配置了,感兴趣的同学可以根据下面的链接自己探索一下
https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/src/libraries/Directory.Build.props#L8
https://github.com/dotnet/runtime/blob/89962a54d60e4d9c9837012d1729c5a72ec748cd/eng/referenceAssemblies.props#L22
References
https://github.com/dotnet/docs/pull/14393
https://github.com/dotnet/docs/issues/2638
https://github.com/dotnet/roslyn/blob/main/docs/features/refout.md
https://docs.microsoft.com/en-us/dotnet/standard/assembly/reference-assemblies
https://docs.microsoft.com/zh-cn/dotnet/standard/assembly/reference-assemblies
https://www.youtube.com/watch?v=EBpY1UMHDY8&list=PLRAdsfhKI4OX1cBGL2IXuEq1yzpDyKlwf&index=1&t=3s