前言今天,.NET官博宣布发布C#9SourceGenerators首个预览版。这是用户呼唤了近5年的功能,今天终于发布了。SourceGenerators,顾名思义,是一个代码生成器,它允许开发者在代码编译过程中获取和查看用户代码并生成新的C#代码参与编译过程,并且可以很好地与代码分析器集成,提供Intellisense,调试信息和错误信息。它可以用于代码生成,所以相当于编译时反射的增强版。使用SourceGenerators,你可以做这些事情:获得一个Compilation对象,它代表所有正在编译的用户代码,你可以从中获得AST和语义模型等信息。可以在Compilation对象中插入新代码,让编译器连同现有用户代码一起编译,SourceGenerators作为编译过程中的一个阶段:编译运行->【分析源代码->生成新代码】->添加生成的新代码进入编译过程->编译继续。上述过程中,方括号中的内容为SourceGenerators参与的阶段和可以做的事情。功能。NET明明有runtimereflection和dynamicILweaving的功能,那么这个SourceGenerators有什么用呢?编译时反射——0运行时开销以ASP.NETCore为例。当启动一个ASP.NETCore应用程序时,它首先会通过运行时反射找到Controllers、Services等的类型定义,然后需要在请求管道中通过运行时反射获取其构造函数信息进行依赖注入。但是,运行时反射非常昂贵。即使缓存了类型签名,对刚启动的应用也没有帮助,也不利于AOT编译。SourceGenerators会让ASP.NETCore在编译时完成所有的类型发现、依赖注入等,并编译成最终的程序集,最终实现0运行时反射,既有利于AOT编译,又是0运行时高架。除了以上功能,gRPC等在编译时也可以使用该功能编织代码参与编译,无需使用任何MSBuildTask进行代码生成!另外,甚至可以读取XML和JSON直接生成C#代码参与编译,完全自动化DTO编写也没有问题。AOT编译SourceGenerators的另一个功能是帮助消除AOT编译优化的主要障碍。许多框架和库大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NETCore和WPF,以在运行时从用户代码中发现类型。这些对于AOT编译优化非常不利,因为为了使反射正常工作,必须将大量额外的和可能不必要的类型元数据编译到最终的本机映像中。使用SourceGenerators,你只需要做编译时的代码生成,就可以避免大部分运行时反射的使用,让AOT编译优化工具更好的运行。示例INotifyPropertyChanged写过WPF或UWP的人都知道,为了让ViewModel中的属性更改可发现,您需要实现INotifyPropertyChanged接口,并在每个必需属性的设置器处划分属性更改事件:classMyViewModel:INotifyPropertyChanged{publiceventPropertyChangedEventHandler?PropertyChanged;privatestring_text;publicstringText{get=>_text;set{_text=value;PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(nameof(Text)));}}}属性多了会很繁琐。之前,C#引入了CallerMemberName来使用当有很多简化的属性时:propertyName=null){PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(propertyName));}}即用CallerMemberName表示参数,编译时自动填入调用者的成员名。但还是不方便。现在有了SourceGenerators,我们可以在编译时生成代码来执行此操作。要实现SourceGenerators,我们需要编写一个实现ISourceGenerator并注释Generator的类型。完整的SourceGenerators代码如下:{[生成器]publicclassAutoNotifyGenerator:ISourceGenerator{privateconststringattributeText=@"usingSystem;namespaceAutoNotify{[AttributeUsage(AttributeTargets.Field,Inherited=false,AllowMultiple=false)]sealedclassAutoNotifyAttribute:Attribute{publicAutoNotifyAttribute};Properting(){"publicvoidInitialize(InitializationContextcontext){//注册一个语法接收器,每次生成都会创建一个语法接收器.From(attributeText,Encoding.UTF8));//获取之前的语法接收者if(!(context.SyntaxReceiverisS语法接收者接收者))返回;//创建目标名称的属性CSharpParseOptions=(context.CompilationasCSharpCompilation).SyntaxTrees[0].OptionsasCSharpParseOptions;Compilationcompilation=context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText,Encodings)UTF8),option;//获取新绑定的Attribute,获取INotifyPropertyChangedINamedTypeSymbolattributeSymbol=compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");INamedTypeSymbolnotifySymbol=compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");字段ListfieldSymbols=newSymbolListad.AttributeClass.Equals(attributeSymbol,SymbolEqualityComparer.Default))){fieldSymbols.Add(fieldSymbol);}}}//按类分组字段并生成代码foreach(IGroupinggroupinfieldSymbols.GroupBy(f=>f.ContainingType)){stringclassSource=ProcessClass(group.Key,group.ToList(),attributeSymbol,notifySymbol,context);context.AddSource($"{group.Key.Name}_autoNotify.cs",SourceText.From(classSource,Encoding.UTF8));}}privatestringProcessClass(INamedTypeSymbolclassSymbol,List字段,ISymbolattributeSymbol,ISymbolnotifySymbol,SourceGeneratorContextcontext){if(!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace,SymbolEquality)Default)Compar{//TODO:必须在顶层,生成诊断信息returnnull;}stringnamespaceName=classSymbol.ContainingNamespace.ToDisplayString();//开始构建要生成的代码StringBuildersource=newStringBuilder($@"namespace{namespaceName}{{publicpartialclass{classSymbol.Name}:{notifySymbol.ToDisplayString()}{{");//如果类型没有实现INotifyPropertyChanged,则添加实现//生成属性foreach(IFieldSymbolfieldSymbolinfields){ProcessField(source,fieldSymbol,attributeSymbol);}source.Append("}}");returnsource.ToString();}privatevoidProcessField(StringBuildersource,IFieldSymbolfieldSymbol,ISymbolattributeSymbol){//获取字段名stringfieldName=fieldSymbol.Name;ITypeSymbolfieldType=fieldSymbol.Type;//获取AutoNotifyAttribute及相关数据AttributeDataattributeData=fieldSymbol.GetAttributes().Single(ad=>ad.AttributeClass.Equals(attributeSymbol,SymbolEqualityComparer.Default));TypedConstantoverridenNameOpt=attribute.SingleOrDefault(kvp=>kvp.Key==“PropertyName”).Value;stringpropertyName=chooseName(fieldName,overridenNameOpt);if(propertyName.Length==0||propertyName==fieldName){//TODO:无法处理,生成诊断信息return;}source.Append($@"public{fieldType}{propertyName}{{get{{returnthis.{fieldName};}}set{{this.{fieldName}=value;this.PropertyChanged?.Invoke(this,newSystem.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));}}}}");stringchooseName(stringfieldName,TypedConstantoverridenNameOpt){if(!overridenNameOpt.IsNull){returnoverridenNameOpt.Value.ToString();}fieldName=fieldName.TrimStart('_');if(fieldName.Length==0)returnstring.Empty;if(fieldName.Length==1)returnfieldName.ToUpper();returnfieldName.Substring(0,1).ToUpper()+fieldName.Substring(1);}}//每次生成代码时都会按需创建Syntaxreceiver//当访问每个语法节点时调用编译,我们可以检查节点并保存任何对生成有用的信息publicvoidOnVisitSyntaxNode(SyntaxNodesyntaxNode){//候选任何具有至少一个属性的字段.Count>0){CandidateFields.Add(fieldDeclarationSyntax);}}}}}有了上面的代码生成器,以后我们只需要这样写ViewModel,通知接口的事件触发调用就会自动生成:publicpartialclassMyViewModel{[AutoNotify]privatestring_text="privatefieldtext";[AutoNotify(PropertyName="Count")]privateint_amount=5;}以上代码会自动生成如下代码参与编译:publicpartialclassMyViewModel:System.ComponentModel.INotifyPropertyChanged{publiceventSystem.ComponentModel.PropertyChangedEventHandlerPropertyChanged;publicstringText{get{returnthis._text;}set{this._text=value;this.PropertyChanged?.Invoke(this,newSystem.ComponentModel.PropertyChangedEventArgs(nameof(Text)));}}publicintCount{get{returnthis._amount;}set{this._amount=value;this.PropertyChanged?.Invoke(this,newSystem.ComponentModel.PropertyChangedEventArgs(nameof(Count)));}}}非常方便!使用时,将SourceGenerators部分作为一个独立的.NETStandard2.0程序集使用(2.1暂不支持),通过以下方式导入到你的项目中:注意需要.NET5preview3以上版本,语言版本指定为preview:preview另外,SourceGenerators需要引入两个nuget包:限制源代码生成器仅访问和生成代码,但现有的代码不能修改,这是出于安全考虑,sourcegenerators文档处于早期预览阶段,docs.microsoft.com上没有相关文档,其文档请访问roslyn仓库中的文档:DesigndocumentuseDocumentpostscript目前SourceGenerators还处于非常早期的预览阶段,后期API可能会发生较大的变化,所以现阶段不要用于生产。另外,关于与IDE集成的开发,诊断信息、断点调试信息等也在进行中,敬请期待后续预览版。