您好!欢迎来到源码码网!

C# 9新特性/代码生成器/编译时反射

  • 源码教程
  • 来源:源码码网
  • 编辑:admin
  • 时间:2020-05-21 10:56
  • 阅读:1050

前言


最近 .NET 官方博客宣布 C# 9 Source Generators 第一个预览版发布,这是一个用户已经喊了快 5 年特性,终于发布了。


简介


Source Generators 顾名思义代码生成器,它允许开发者在代码编译过程中获取查看用户代码并且生成新的 C# 代码参与编译过程,并且可以很好的与代码分析器集成提供 Intellisense、调试信息和报错信息,可以用它来做代码生成,因此也相当于是一个加强版本的编译时反射。


使用 Source Generators,可以做到这些事情:


  • 获取一个 Compilation 对象,这个对象表示了所有正在编译的用户代码,你可以从中获取 AST 和语义模型等信息


  • 可以向 Compilation 对象中插入新的代码,让编译器连同已有的用户代码一起编译


Source Generators 作为编译过程中的一个阶段执行:


编译运行 -> [分析源代码 -> 生成新代码] -> 将生成的新代码添加入编译过程 -> 编译继续。


上述流程中,中括号包括的内容即为 Source Generators 所参与的阶段和能做到的事情。


作用


.NET 明明具备运行时反射和动态 IL 织入功能,那这个 Source Generators 有什么用呢?


编译时反射 - 0 运行时开销


拿 ASP.NET Core 举例,启动一个 ASP.NET Core 应用时,首先会通过运行时反射来发现 Controllers、Services 等的类型定义,然后在请求管道中需要通过运行时反射获取其构造函数信息以便于进行依赖注入。


然而运行时反射开销很大,即使缓存了类型签名,对于刚刚启动后的应用也无任何帮助作用,而且不利于做 AOT 编译。


Source Generators 将可以让 ASP.NET Core 所有的类型发现、依赖注入等在编译时就全部完成并编译到最终的程序集当中,最终做到 0 运行时反射使用,不仅利于 AOT 编译,而且运行时 0 开销。


除了上述作用之外,gRPC 等也可以利用此功能在编译时织入代码参与编译,不需要再利用任何的 MSBuild Task 做代码生成啦!


另外,甚至还可以读取 XML、JSON 直接生成 C# 代码参与编译,DTO 编写全自动化都是没问题的。


AOT 编译


Source Generators 的另一个作用是可以帮助消除 AOT 编译优化的主要障碍。


许多框架和库都大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NET Core 和 WPF 等等,它们在运行时从用户代码中发现类型。这些非常不利于 AOT 编译优化,因为为了使反射能够正常工作,必须将大量额外甚至可能不需要的类型元数据编译到最终的原生映像当中。


有了 Source Generators 之后,只需要做编译时代码生成便可以避免大部分的运行时反射的使用,让 AOT 编译优化工具能够更好的运行。


例子


INotifyPropertyChanged


写过 WPF 或 UWP 的都知道,在 ViewModel 中为了使属性变更可被发现,需要实现 INotifyPropertyChanged 接口,并且在每一个需要的属性的 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)));
       }
   }
}


当属性多了之后将会非常繁琐,先前 C# 引入了 CallerMemberName 用于简化属性较多时候的情况:


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 指示参数,在编译时自动填充调用方的成员名称。


但是还是不方便。


如今有了 Source Generators,我们可以在编译时生成代码做到这一点了。


为了实现 Source Generators,我们需要写个实现了 ISourceGenerator 并且标注了 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)
       
{
           // 注册一个语法接收器,会在每次生成时被创建
           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;
           // 创建处目标名称的属性
           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 标注的字段
           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)
               {
                   // 获取字段符号信息,如果有 AutoNotify 标注则保存
                   IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                   if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                   {
                       fieldSymbols.Add(fieldSymbol);
                   }
               }
           }
           // 按 class 对字段进行分组,并生成代码
           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: 必须在顶层,产生诊断信息
               return null;
           }
           string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
           // 开始构建要生成的代码
           StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{{
   public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
   {{
"
);
           // 如果类型还没有实现 INotifyPropertyChanged 则添加实现
           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 和相关的数据
           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: 无法处理,产生诊断信息
               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);
           }
       }
       // 语法接收器,将在每次生成代码时被按需创建
       class SyntaxReceiver : ISyntaxReceiver
       {
           public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
           // 编译中在访问每个语法节点时被调用,我们可以检查节点并保存任何对生成有用的信息
           public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
           
{
               // 将具有至少一个 Attribute 的任何字段作为候选
               if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                   && fieldDeclarationSyntax.AttributeLists.Count > 0)
               {
                   CandidateFields.Add(fieldDeclarationSyntax);
               }
           }
       }
  }
}


有了上述代码生成器之后,以后我们只需要这样写 ViewModel 就会自动生成通知接口的事件触发调用:


public partial class MyViewModel
{
   [AutoNotify]
   private string _text = "private field text";
   [AutoNotify(PropertyName = "Count")]
   private int _amount = 5;
}


上述代码将会在编译时自动生成以下代码参与编译:


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)));
       }
   }
}


非常方便!


使用时,将 Source Generators 部分作为一个独立的 .NET Standard 2.0 程序集(暂时不支持 2.1),用以下方式引入到你的项目即可:


<ItemGroup>
 <Analyzer Include="..MySourceGeneratorin$(Configuration) etstandard2.0MySourceGenerator.dll" />
</ItemGroup>

<ItemGroup>
 <ProjectReference Include="..MySourceGeneratorMySourceGenerator.csproj" />
</ItemGroup>


注意需要最新的 .NET 5 preview(写文章时还在 artifacts 里没正式 release),并指定语言版本为 preview:


<PropertyGroup>
 <LangVersion>preview</LangVersion>
</PropertyGroup>


另外,Source Generators 需要引入两个 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 处于早期预览阶段,docs.microsoft.com 上暂时没有相关文档,关于它的文档请访问在 roslyn 仓库中的文档:


设计文档:https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md


使用文档:https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md


后记


目前 Source Generators 仍处于非常早期的预览阶段,API 后期还可能会有很大的改动,因此现阶段不要用于生产。


另外,关于与 IDE 的集成、诊断信息、断点调试信息等的开发也在进行中,请期待后续的 preview 版本吧。


声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

特别声明:
1、如无特殊说明,内容均为本站原创发布,转载请注明出处;
2、部分转载文章已注明出处,转载目的为学习和交流,如有侵犯,请联系客服删除;
3、编辑非《源码码网》的文章均由用户编辑发布,不代表本站立场,如涉及侵犯,请联系删除;
全部评论(0)
推荐阅读
  • 陀螺匠OA办公系统源码 PHP + Vue 前后端分离 完整可商用
  • 陀螺匠OA办公系统源码 PHP + Vue 前后端分离 完整可商用
  • 项目介绍《陀螺匠》是一套采用Laravel9框架结合Swoole高性能框架与Vue.js前端技术栈构建的AI+低代码双引擎智能办公系统。陀螺匠深度融合了AI智能体以及低代码开发平台,系统简约,易于功能扩展,方便二次开发。旨在为企业提供一站式、数智化转型的全方位解决方案,助力企业高效运营与智能决策。源码说明源码说明:项目代码100%开源,基于AGPL-3.0,支持免费商用!技术栈:系统基于Laravel9+Swoole+Vue.
  • 软件教程
  • 来源:CRMEB
  • 编辑:CRMEB
  • 时间:2026-03-27 15:33
  • 阅读:654
  • open claw最近很火,如何理性看待?
  • open claw最近很火,如何理性看待?
  • openclaw是2026年初科技圈最火爆的现象级话题。它不仅仅是又一个AI产品,更被百川智能的王小川誉为与ChatGPT诞生同等重要的里程碑——标志着AI从”只会说”的对话者,进化到了”能够做”的行动者。对于这股全民”养虾”热潮,我的观察是:它代表了一次人机关系的革命性跃迁,但同时也是一场充满风险与泡沫的狂欢,其真正的价值需要在喧嚣褪去、安全与成本的挑战被克服后才能显现。下
  • 行业资讯
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-03-17 11:06
  • 阅读:716
  • 原生小程序和封装的小程序有什么区别
  • 原生小程序和封装的小程序有什么区别
  • 原生小程序和封装的小程序是两种不同的开发模式,主要区别在于开发方式、性能表现、代码维护以及跨平台能力。为了让你更清晰地理解,我们可以把“原生”比作用钢筋水泥直接盖楼,而“封装”则是用预制板或套件来组装楼。以下是详细的对比分析:1.定义与本质原生小程序:定义: 直接使用微信官方提供的开发工具、语法和API进行开发。语言: 使用微信自己定义的一套技术栈(WXML(微信标记语言)做结构、WXSS(微信样式表)做样式、JS
  • 行业资讯
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-03-06 11:54
  • 阅读:944
  • 基于NetCore(Razor Page)开发的Cms建站系统MIT协议
  • 基于NetCore(Razor Page)开发的Cms建站系统MIT协议
  • FytSoaCms一款基于NetCore2.2/3.1(RazorPage)功能强大的Cms建站系统,支持前后端分离。简介:模块化:全新的架构和模块化的开发机制,便于灵活扩展和二次开发。模型/栏目/分类信息体系:通过栏目和模型绑定,以及不同的模型类型,不同栏目可以实现差异化的功能,轻松实现诸如资讯、下载、讨论和图片等功能。通过分类信息和栏目绑定,可以自动建立索引表,轻松实现复杂的信息检索。FytSoa是一套基于NetCore+SqlS
  • 开发工具
  • 来源:gitee
  • 编辑:源码码网
  • 时间:2026-01-24 22:21
  • 阅读:799
  • 基于JAVA开发的企业级平台微信公众号管理系统SmartWx
  • 基于JAVA开发的企业级平台微信公众号管理系统SmartWx
  • SmartWx微信公众号管理系统是一个完整的微信公众号web操作版,直接编译即可运行。让用户不再用关心微信的底层接口,直接使用页面进行操作,简单方便。包括服务器绑定、文本管理、图文管理、菜单管理、粉丝管理、群发消息等。技术框架开发语言:JAVA数据库:MYSQLJAVA开发框架:SpringMVC+Spring+Mybatis缓存框架:j2cache前端开发框架:Layui+JQuery+html前台模板引擎:art-templat
  • 源码教程
  • 来源:gitee
  • 编辑:源码码网
  • 时间:2026-01-24 21:53
  • 阅读:833
联系客服
源码代售 源码咨询 技术开发 联系客服
029-84538663
手机版

扫一扫进手机版
返回顶部