您好!欢迎来到源码码网

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

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

前言


最近 .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)
推荐阅读
  • 常用测试压力工具使用介绍
  • 常用测试压力工具使用介绍
  • ab 是 ApacheBench 工具的缩写,它是一个HTTP压力测试工具。让我详细说明如何测试:1. 安装ApacheBenchWindows系统:方法一:安装XAMPP或WAMP(自带ab)下载地址:https://www.apachefriends.org/zh_cn/index.html安装后,ab工具在:C:xamppapacheinab.exe方法二:使
  • 开发工具
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-01-13 20:27
  • 阅读:64
  • 工程项目一体化自动管理软件解决方案
  • 工程项目一体化自动管理软件解决方案
  • 1.项目概述1.1项目背景在工程建设行业数字化转型浪潮下,传统项目管理面临信息孤岛、协同困难、进度不可控、成本超支等痛点。本方案旨在构建一个覆盖工程项目全生命周期、全参与方、全业务流程的一体化智能管理平台。1.2解决方案愿景打造数据驱动、智能协同、风险预警、自动执行的工程大脑,实现:管理流程自动化率≥80%项目协同效率提升40%成本偏差率降低至±3%以内安全事故发生率降低60%1.3目标用户矩阵┌───────────────┬
  • 行业资讯
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-01-09 11:26
  • 阅读:171
  • 车辆管理系统需求文档与技术架构PC端+小程序
  • 车辆管理系统需求文档与技术架构PC端+小程序
  • 第一部分:需求文档1.项目概述1.1项目背景为企事业单位、车队运营商、租赁公司等提供一套完整的车辆全生命周期管理解决方案,实现车辆管理数字化、智能化。1.2项目目标建立车辆从购置到报废的全流程管理体系实现用车申请、调度、监控、结算的闭环管理通过数据分析优化车辆使用效率降低车辆运维成本20%以上1.3用户角色矩阵┌──────────────┬─────────────────────────────┬──────────────
  • 行业资讯
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-01-09 11:11
  • 阅读:165
  • 智慧农业/渔业物联网系统需求文档
  • 智慧农业/渔业物联网系统需求文档
  • 智慧农业/渔业物联网系统需求文档文档版本: V1.0项目目标: 构建一个集环境智能监测、设备自动化控制、生长模型分析、溯源管理与远程指挥于一体的综合物联网管理平台,实现降本增效、提质增产、风险预警与品牌增值。1.系统总体概述1.1核心价值: 数据驱动决策,解放人力,实现农业/渔业生产的精准化、自动化与智能化。1.2用户角色:生产员/养殖员: 现场巡视、接收告警、执行设备手动控制、查看实时环境
  • 行业资讯
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-01-09 11:04
  • 阅读:71
  • 程序员AI编程工具推荐
  • 程序员AI编程工具推荐
  • AI编程工具是当前开发者的“副驾驶”,能够极大提升开发效率。以下我将从通用型、代码专用型、垂直领域型以及开源/自部署型几个维度为您分类推荐,并附上它们的核心特点和适用场景,帮助您选择。一、通用型AI对话助手(编程是核心能力之一)这类工具本质是“更懂代码的ChatGPT”,适合处理广泛的编程问题、解释代码、生成文档等。ChatGPT(GPT-4/4o)简介:行业标杆,尤其在GPT-4版本下,代码理解和生成能力极强。优点:上下文能力强,
  • 源码教程
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2026-01-09 10:56
  • 阅读:99
联系客服
源码代售 源码咨询 技术开发 联系客服
029-84538663
手机版

扫一扫进手机版
返回顶部