文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-24 18:09

关注

简介

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

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

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 处除法属性更改事件:

  1. class MyViewModel : INotifyPropertyChanged 
  2. public event PropertyChangedEventHandler? PropertyChanged; 
  3.  
  4. private string _text; 
  5. public string Text 
  6. get => _text; 
  7. set 
  8. _text = value; 
  9. PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(nameof(Text))); 

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

  1. class MyViewModel : INotifyPropertyChanged 
  2. public event PropertyChangedEventHandler? PropertyChanged; 
  3.  
  4. private string _text; 
  5. public string Text 
  6. get => _text; 
  7. set 
  8. _text = value; 
  9. OnPropertyChanged(); 
  10.  
  11. protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null
  12. PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(propertyName)); 

即,用 CallerMemberName 指示参数,在编译时自动填充调用方的成员名称。

但是还是不方便。

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

为了实现 Source Generators,我们需要写个实现了 ISourceGenerator 并且标注了 Generator的类型。

完整的 Source Generators 代码如下:

  1. using System; 
  2. using System.Collections.Generic; 
  3. using System.Linq; 
  4. using System.Text; 
  5. using Microsoft.CodeAnalysis; 
  6. using Microsoft.CodeAnalysis.CSharp; 
  7. using Microsoft.CodeAnalysis.CSharp.Syntax; 
  8. using Microsoft.CodeAnalysis.Text; 
  9.  
  10. namespace MySourceGenerator 
  11. [Generator] 
  12. public class AutoNotifyGenerator : ISourceGenerator 
  13. private const string attributeText = @" 
  14. using System; 
  15. namespace AutoNotify 
  16. [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] 
  17. sealed class AutoNotifyAttribute : Attribute 
  18. public AutoNotifyAttribute() 
  19. public string PropertyName { get; set; } 
  20. "; 
  21.  
  22. public void Initialize(InitializationContext context) 
  23. // 注册一个语法接收器,会在每次生成时被创建 
  24. context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 
  25.  
  26. public void Execute(SourceGeneratorContext context) 
  27. // 添加 Attrbite 文本 
  28. context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8)); 
  29.  
  30. // 获取先前的语法接收器  
  31. if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) 
  32. return
  33.  
  34. // 创建处目标名称的属性 
  35. CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; 
  36. Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options)); 
  37.  
  38. // 获取新绑定的 Attribute,并获取INotifyPropertyChanged 
  39. INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute"); 
  40. INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); 
  41.  
  42. // 遍历字段,只保留有 AutoNotify 标注的字段 
  43. List fieldSymbols = new List(); 
  44. foreach (FieldDeclarationSyntax field in receiver.CandidateFields) 
  45. SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree); 
  46. foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables) 
  47. // 获取字段符号信息,如果有 AutoNotify 标注则保存 
  48. IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol; 
  49. if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default))) 
  50. fieldSymbols.Add(fieldSymbol); 
  51.  
  52. // 按 class 对字段进行分组,并生成代码 
  53. foreach (IGrouping group in fieldSymbols.GroupBy(f => f.ContainingType)) 
  54. string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); 
  55. context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8)); 
  56.  
  57. private string ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context) 
  58. if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) 
  59. // TODO: 必须在顶层,产生诊断信息 
  60. return null
  61.  
  62. string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); 
  63.  
  64. // 开始构建要生成的代码 
  65. StringBuilder source = new StringBuilder($@" 
  66. namespace {namespaceName} 
  67. {{ 
  68. public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} 
  69. {{ 
  70. "); 
  71.  
  72. // 如果类型还没有实现 INotifyPropertyChanged 则添加实现 
  73. if (!classSymbol.Interfaces.Contains(notifySymbol)) 
  74. source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); 
  75.  
  76. // 生成属性 
  77. foreach (IFieldSymbol fieldSymbol in fields) 
  78. ProcessField(source, fieldSymbol, attributeSymbol); 
  79.  
  80. source.Append("} }"); 
  81. return source.ToString(); 
  82.  
  83. private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) 
  84. // 获取字段名称 
  85. string fieldName = fieldSymbol.Name; 
  86. ITypeSymbol fieldType = fieldSymbol.Type; 
  87.  
  88. // 获取 AutoNotify Attribute 和相关的数据 
  89. AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); 
  90. TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; 
  91.  
  92. string propertyName = chooseName(fieldName, overridenNameOpt); 
  93. if (propertyName.Length == 0 || propertyName == fieldName) 
  94. //TODO: 无法处理,产生诊断信息 
  95. return
  96.  
  97. source.Append($@" 
  98. public {fieldType} {propertyName}  
  99. {{ 
  100. get  
  101. {{ 
  102. return this.{fieldName}; 
  103. }} 
  104. set 
  105. {{ 
  106. this.{fieldName} = value; 
  107. this.PropertyChanged?.Invoke(thisnew System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName}))); 
  108. }} 
  109. }} 
  110. "); 
  111.  
  112. string chooseName(string fieldName, TypedConstant overridenNameOpt) 
  113. if (!overridenNameOpt.IsNull) 
  114. return overridenNameOpt.Value.ToString(); 
  115.  
  116. fieldName = fieldName.TrimStart('_'); 
  117. if (fieldName.Length == 0
  118. return string.Empty; 
  119.  
  120. if (fieldName.Length == 1
  121. return fieldName.ToUpper(); 
  122.  
  123. return fieldName.Substring(01).ToUpper() + fieldName.Substring(1); 
  124.  
  125.  
  126. // 语法接收器,将在每次生成代码时被按需创建 
  127. class SyntaxReceiver : ISyntaxReceiver 
  128. public List CandidateFields { get; } = new List(); 
  129.  
  130. // 编译中在访问每个语法节点时被调用,我们可以检查节点并保存任何对生成有用的信息 
  131. public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 
  132. // 将具有至少一个 Attribute 的任何字段作为候选 
  133. if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax 
  134. && fieldDeclarationSyntax.AttributeLists.Count > 0
  135. CandidateFields.Add(fieldDeclarationSyntax); 

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

  1. public partial class MyViewModel 
  2. [AutoNotify] 
  3. private string _text = "private field text"
  4.  
  5. [AutoNotify(PropertyName = "Count")] 
  6. private int _amount = 5

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

  1. public partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged 
  2. public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; 
  3.  
  4. public string Text 
  5. get  
  6. return this._text; 
  7. set 
  8. this._text = value; 
  9. this.PropertyChanged?.Invoke(thisnew System.ComponentModel.PropertyChangedEventArgs(nameof(Text))); 
  10.  
  11. public int Count 
  12. get  
  13. return this._amount; 
  14. set 
  15. this._amount = value; 
  16. this.PropertyChanged?.Invoke(thisnew System.ComponentModel.PropertyChangedEventArgs(nameof(Count))); 

非常方便!

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

  1.  
  2. "..\MySourceGenerator\bin\$(Configuration)\netstandard2.0\MySourceGenerator.dll" /> 
  3.  
  4.  
  5.  
  6. "..\MySourceGenerator\MySourceGenerator.csproj" /> 
  7.  

注意需要 .NET 5 preview 3 或以上版本,并指定语言版本为 preview :

  1.  
  2. preview 
  3.  

另外,Source Generators 需要引入两个 nuget 包:

  1.  
  2. "Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" /> 
  3. "Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" /> 
  4.  

限制

Source Generators 仅能用于访问和生成代码,但是不能修改已有代码,这有一定原因是出于安全考量。

文档

Source Generators 处于早期预览阶段,docs.microsoft.com 上暂时没有相关文档,关于它的文档请访问在 roslyn 仓库中的文档:

设计文档

使用文档

后记

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

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

 

来源:博客园内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯