.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