Source Generator 是微軟於 .NET 5 所推出的新功能,
它允許我們從原始碼編譯的結果中取得所需的 meta 資訊,
進而根據這些資訊去組出額外的程式碼,並加至最後的編譯結果中。
而當原始碼數量過於龐大時,將篩選 Syntax 的邏輯寫在 Generator 內就會稍顯雜亂。
這時可以使用 SyntaxReceiver 幫助我們快速篩選所需的 Syntax 資訊!
前言 & 作業環境
前面提到,SyntaxRecevier 可以幫助我們快速篩選所需的 Syntax 資訊,
在實作開始前,請先檢查是否以滿足所需的作業環境。
- Visual Studio 2019 v16.8.0 ↑
- .NET 5 ( SDK 5.0.100 )
本文適合對 Source Generator 有初步理解的朋友閱讀
第一次接觸的朋友建議可先閱讀 Introducing C# Source Generators 。
實作 SyntaxReceiver
整體作法不算太難,
首先請先實作 ISyntaxRecevier 介面,
public class MySyntaxReceiver : ISyntaxReceiver
{
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
throw new NotImplementedException();
}
}
這個介面只有一個方法 OnVisitSyntaxNode,
你可以當作 Generator 會把原始碼編譯過的每個 SyntaxNode 丟進來檢查,
若符合想要的條件時,我們可以透過物件屬性 ( Property ) 將它保留下來。
舉例來說,如果想要快速的篩選出類別名稱為 MyClass 的 Syntax 資訊,
我們可以先檢查 SyntaxNode 是否為類別宣告用的 ClassDeclarationSyntax,
若符合,則透過 Identifier.ValueText 比對類別名稱,
程式碼如下:
public class MySyntaxReceiver : ISyntaxReceiver
{
public ClassDeclarationSyntax MyClassSyntax { get; set; }
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classSyntax &&
classSyntax.Identifier.ValueText == "MyClass")
{
MyClassSyntax = classSyntax;
}
}
}
完成後回到 Generator 的 Initialize 方法,
透過 GeneratorInitializationContext 註冊 MySyntaxRecevier
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
最後於 Generator 執行階段的 Execute 方法,
再從 GeneratorExecutionContext 身上將 SyntaxReceiver 取出,
做個簡單的轉型後就可以取回 MySyntaxReceiver 身上的屬性資訊了!
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is MySyntaxReceiver mySyntaxReceiver)
{
ClassDeclarationSyntax classSyntax = mySyntaxReceiver.MyClassSyntax;
}
}
探討 - 可否實作多個 SyntaxRecevier?
在好奇之下我嘗試了實作多個 SyntaxReceiver,
先說結果:「不行,會噴例外。」
這邊我額外註冊了另一個 OtherSyntaxRecevier,程式碼如下。
public void Initialize(GeneratorInitializationContext context)
{
//手動呼叫偵錯器
Debugger.Launch();
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
context.RegisterForSyntaxNotifications(() => new OtherSyntaxReceiver());
}
使用偵錯模式就會得到例外 ( 如下圖 )。
從註冊的 RegisterForSyntaxNotifications 方法追了一下,
發現內部會先檢查 InfoBuilder 本身是否已持有 SyntaxReceiverCreator,
如果是的話就拋出例外,程式碼如下。
public void RegisterForSyntaxNotifications(SyntaxReceiverCreator receiverCreator)
{
CheckIsEmpty(InfoBuilder.SyntaxReceiverCreator);
InfoBuilder.SyntaxReceiverCreator = receiverCreator;
}
private static void CheckIsEmpty<T>(T x)
{
if (x is object)
{
throw new InvalidOperationException(string.Format(CodeAnalysisResources.Single_type_per_generator_0, typeof(T).Name));
}
}
而 SyntaxReceiverCreator 本身其實是一個回傳 ISyntaxReceiver 的委派,
public delegate ISyntaxReceiver SyntaxReceiverCreator();
結語
Source Generator 雖伴隨著 .NET 5 的發布而釋出,
其「無法修改原始碼」的特性讓人又愛又恨,
目前整體來說用起來仍稱不上「順手」。
但官方在 .NET 5 的釋出文件中提到:
We expect to make more use of source generators within the .NET product in .NET 6.0 and beyond.
讓我們期待一下它在 .NET 6 會有怎樣的表現吧!
參考
Roslyn - source-generator.cookbook ( GitHub)