C# 9 新特性:代碼生成器、編譯時(shí)反射
前言
今天 .NET 官方博客宣布 C# 9 Source Generators 第一個(gè)預(yù)覽版發(fā)布,這是一個(gè)用戶已經(jīng)喊了快 5 年特性,今天終于發(fā)布了。
簡(jiǎn)介
Source Generators 顧名思義代碼生成器,它允許開發(fā)者在代碼編譯過程中獲取查看用戶代碼并且生成新的 C# 代碼參與編譯過程,并且可以很好的與代碼分析器集成提供 Intellisense、調(diào)試信息和報(bào)錯(cuò)信息,可以用它來做代碼生成,因此也相當(dāng)于是一個(gè)加強(qiáng)版本的編譯時(shí)反射。
使用 Source Generators,可以做到這些事情:
- 獲取一個(gè) Compilation 對(duì)象,這個(gè)對(duì)象表示了所有正在編譯的用戶代碼,你可以從中獲取 AST 和語義模型等信息
- 可以向 Compilation 對(duì)象中插入新的代碼,讓編譯器連同已有的用戶代碼一起編譯
Source Generators 作為編譯過程中的一個(gè)階段執(zhí)行:
編譯運(yùn)行 -> [分析源代碼 -> 生成新代碼] -> 將生成的新代碼添加入編譯過程 -> 編譯繼續(xù)。
上述流程中,中括號(hào)包括的內(nèi)容即為 Source Generators 所參與的階段和能做到的事情。
作用
.NET 明明具備運(yùn)行時(shí)反射和動(dòng)態(tài) IL 織入功能,那這個(gè) Source Generators 有什么用呢?
編譯時(shí)反射 - 0 運(yùn)行時(shí)開銷
拿 ASP.NET Core 舉例,啟動(dòng)一個(gè) ASP.NET Core 應(yīng)用時(shí),首先會(huì)通過運(yùn)行時(shí)反射來發(fā)現(xiàn) Controllers、Services 等的類型定義,然后在請(qǐng)求管道中需要通過運(yùn)行時(shí)反射獲取其構(gòu)造函數(shù)信息以便于進(jìn)行依賴注入。然而運(yùn)行時(shí)反射開銷很大,即使緩存了類型簽名,對(duì)于剛剛啟動(dòng)后的應(yīng)用也無任何幫助作用,而且不利于做 AOT 編譯。
Source Generators 將可以讓 ASP.NET Core 所有的類型發(fā)現(xiàn)、依賴注入等在編譯時(shí)就全部完成并編譯到最終的程序集當(dāng)中,最終做到 0 運(yùn)行時(shí)反射使用,不僅利于 AOT 編譯,而且運(yùn)行時(shí) 0 開銷。
除了上述作用之外,gRPC 等也可以利用此功能在編譯時(shí)織入代碼參與編譯,不需要再利用任何的 MSBuild Task 做代碼生成啦!
另外,甚至還可以讀取 XML、JSON 直接生成 C# 代碼參與編譯,DTO 編寫全自動(dòng)化都是沒問題的。
AOT 編譯
Source Generators 的另一個(gè)作用是可以幫助消除 AOT 編譯優(yōu)化的主要障礙。
許多框架和庫都大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NET Core 和 WPF 等等,它們?cè)谶\(yùn)行時(shí)從用戶代碼中發(fā)現(xiàn)類型。這些非常不利于 AOT 編譯優(yōu)化,因?yàn)闉榱耸狗瓷淠軌蛘9ぷ鳎仨殞⒋罅款~外甚至可能不需要的類型元數(shù)據(jù)編譯到最終的原生映像當(dāng)中。
有了 Source Generators 之后,只需要做編譯時(shí)代碼生成便可以避免大部分的運(yùn)行時(shí)反射的使用,讓 AOT 編譯優(yōu)化工具能夠更好的運(yùn)行。
例子
INotifyPropertyChanged
寫過 WPF 或 UWP 的都知道,在 ViewModel 中為了使屬性變更可被發(fā)現(xiàn),需要實(shí)現(xiàn) INotifyPropertyChanged
接口,并且在每一個(gè)需要的屬性的 setter
處除法屬性更改事件:
- class MyViewModel : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler? PropertyChanged;
- private string _text;
- public string Text
- {
- get => _text;
- set
- {
- _text = value;
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
- }
- }
- }
當(dāng)屬性多了之后將會(huì)非常繁瑣,先前 C# 引入了 CallerMemberName
用于簡(jiǎn)化屬性較多時(shí)候的情況:
- class MyViewModel : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler? PropertyChanged;
- private string _text;
- public string Text
- {
- get => _text;
- set
- {
- _text = value;
- OnPropertyChanged();
- }
- }
- protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
即,用 CallerMemberName
指示參數(shù),在編譯時(shí)自動(dòng)填充調(diào)用方的成員名稱。
但是還是不方便。
如今有了 Source Generators,我們可以在編譯時(shí)生成代碼做到這一點(diǎn)了。
為了實(shí)現(xiàn) Source Generators,我們需要寫個(gè)實(shí)現(xiàn)了 ISourceGenerator
并且標(biāo)注了 Generator
的類型。
完整的 Source Generators 代碼如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using Microsoft.CodeAnalysis;
- using Microsoft.CodeAnalysis.CSharp;
- using Microsoft.CodeAnalysis.CSharp.Syntax;
- using Microsoft.CodeAnalysis.Text;
- namespace MySourceGenerator
- {
- [Generator]
- public class AutoNotifyGenerator : ISourceGenerator
- {
- private const string attributeText = @"
- using System;
- namespace AutoNotify
- {
- [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
- sealed class AutoNotifyAttribute : Attribute
- {
- public AutoNotifyAttribute()
- {
- }
- public string PropertyName { get; set; }
- }
- }
- ";
- public void Initialize(InitializationContext context)
- {
- // 注冊(cè)一個(gè)語法接收器,會(huì)在每次生成時(shí)被創(chuàng)建
- context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
- }
- public void Execute(SourceGeneratorContext context)
- {
- // 添加 Attrbite 文本
- context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));
- // 獲取先前的語法接收器
- if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
- return;
- // 創(chuàng)建處目標(biāo)名稱的屬性
- CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
- Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
- // 獲取新綁定的 Attribute,并獲取INotifyPropertyChanged
- INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
- INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
- // 遍歷字段,只保留有 AutoNotify 標(biāo)注的字段
- List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
- foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
- {
- SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
- foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
- {
- // 獲取字段符號(hào)信息,如果有 AutoNotify 標(biāo)注則保存
- IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
- if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
- {
- fieldSymbols.Add(fieldSymbol);
- }
- }
- }
- // 按 class 對(duì)字段進(jìn)行分組,并生成代碼
- foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
- {
- string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
- context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
- }
- }
- private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
- {
- if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
- {
- // TODO: 必須在頂層,產(chǎn)生診斷信息
- return null;
- }
- string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
- // 開始構(gòu)建要生成的代碼
- StringBuilder source = new StringBuilder($@"
- namespace {namespaceName}
- {{
- public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
- {{
- ");
- // 如果類型還沒有實(shí)現(xiàn) INotifyPropertyChanged 則添加實(shí)現(xiàn)
- if (!classSymbol.Interfaces.Contains(notifySymbol))
- {
- source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
- }
- // 生成屬性
- foreach (IFieldSymbol fieldSymbol in fields)
- {
- ProcessField(source, fieldSymbol, attributeSymbol);
- }
- source.Append("} }");
- return source.ToString();
- }
- private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
- {
- // 獲取字段名稱
- string fieldName = fieldSymbol.Name;
- ITypeSymbol fieldType = fieldSymbol.Type;
- // 獲取 AutoNotify Attribute 和相關(guān)的數(shù)據(jù)
- AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
- TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;
- string propertyName = chooseName(fieldName, overridenNameOpt);
- if (propertyName.Length == 0 || propertyName == fieldName)
- {
- //TODO: 無法處理,產(chǎn)生診斷信息
- return;
- }
- source.Append($@"
- public {fieldType} {propertyName}
- {{
- get
- {{
- return this.{fieldName};
- }}
- set
- {{
- this.{fieldName} = value;
- this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
- }}
- }}
- ");
- string chooseName(string fieldName, TypedConstant overridenNameOpt)
- {
- if (!overridenNameOpt.IsNull)
- {
- return overridenNameOpt.Value.ToString();
- }
- fieldName = fieldName.TrimStart('_');
- if (fieldName.Length == 0)
- return string.Empty;
- if (fieldName.Length == 1)
- return fieldName.ToUpper();
- return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
- }
- }
- // 語法接收器,將在每次生成代碼時(shí)被按需創(chuàng)建
- class SyntaxReceiver : ISyntaxReceiver
- {
- public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
- // 編譯中在訪問每個(gè)語法節(jié)點(diǎn)時(shí)被調(diào)用,我們可以檢查節(jié)點(diǎn)并保存任何對(duì)生成有用的信息
- public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
- {
- // 將具有至少一個(gè) Attribute 的任何字段作為候選
- if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
- && fieldDeclarationSyntax.AttributeLists.Count > 0)
- {
- CandidateFields.Add(fieldDeclarationSyntax);
- }
- }
- }
- }
- }
有了上述代碼生成器之后,以后我們只需要這樣寫 ViewModel 就會(huì)自動(dòng)生成通知接口的事件觸發(fā)調(diào)用:
- public partial class MyViewModel
- {
- [AutoNotify]
- private string _text = "private field text";
- [AutoNotify(PropertyName = "Count")]
- private int _amount = 5;
- }
上述代碼將會(huì)在編譯時(shí)自動(dòng)生成以下代碼參與編譯:
- public partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged
- {
- public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
- public string Text
- {
- get
- {
- return this._text;
- }
- set
- {
- this._text = value;
- this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Text)));
- }
- }
- public int Count
- {
- get
- {
- return this._amount;
- }
- set
- {
- this._amount = value;
- this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Count)));
- }
- }
- }
非常方便!
使用時(shí),將 Source Generators 部分作為一個(gè)獨(dú)立的 .NET Standard 2.0 程序集(暫時(shí)不支持 2.1),用以下方式引入到你的項(xiàng)目即可:
- <ItemGroup>
- <Analyzer Include="..\MySourceGenerator\bin\$(Configuration)\netstandard2.0\MySourceGenerator.dll" />
- </ItemGroup>
- <ItemGroup>
- <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" />
- </ItemGroup>
注意需要 .NET 5 preview 3 或以上版本,并指定語言版本為 preview
:
- <PropertyGroup>
- <LangVersion>preview</LangVersion>
- </PropertyGroup>
另外,Source Generators 需要引入兩個(gè) nuget 包:
- <ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" />
- <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
- </ItemGroup>
限制
Source Generators 僅能用于訪問和生成代碼,但是不能修改已有代碼,這有一定原因是出于安全考量。
文檔
Source Generators 處于早期預(yù)覽階段,docs.microsoft.com 上暫時(shí)沒有相關(guān)文檔,關(guān)于它的文檔請(qǐng)?jiān)L問在 roslyn 倉庫中的文檔:
后記
目前 Source Generators 仍處于非常早期的預(yù)覽階段,API 后期還可能會(huì)有很大的改動(dòng),因此現(xiàn)階段不要用于生產(chǎn)。
另外,關(guān)于與 IDE 的集成、診斷信息、斷點(diǎn)調(diào)試信息等的開發(fā)也在進(jìn)行中,請(qǐng)期待后續(xù)的 preview 版本吧。