找回密码
 立即注册
首页 业界区 业界 Roslyn 技术解析:如何利用它做代码规范检查与运行时代 ...

Roslyn 技术解析:如何利用它做代码规范检查与运行时代码生成?​

辖瑁地 2025-10-15 21:20:06
1.什么是 Roslyn

聊起 Roslyn 可能对于有部分小伙伴有些陌生,有些小伙听过但是没接触过,有些小伙伴可能比较擅长,其实在这之前我也是个懵的,听过但是没深入了解,因为我不知道并不影响我做一些增删改查,但是如果你要深入,或者写一些框架底层或者提升效率的工具以及扩展,那这个是必须掌握的技术。
年初时,我在与技术大牛 痴者工良  交流的过程中,算是正式接触到 Roslyn,瞬间被它的强大能力所吸引。他深入浅出的讲解让我意识到,这不仅是编译器黑科技,更是提升代码质量与开发效率的利器。受他启发,我开始系统学习,虽断断续续折腾了一阵,但一直未做总结。最近终于得空,便将所学梳理成文,分享出来,既是记录,也是致敬好朋友严架的帮助。
在正式认识 Roslyn 之前,我们必须先对咱们 C# .NET 的编译流程有个大概了解,当然 VB.NET 也适用,但是接受不来他的语法,有些小伙伴可能知道或者了解,简单的给个图感受一下。

1. C# / .NET 编译流程简述


  • 源代码阶段:我们手动写出 C# 或者 VB.NET 代码
  • 编译器阶段:Roslyn 编译器将源代码转换为 IL(Intermediate Language)中间代码
  • IL 生成:生成 .dll 或 .exe 文件包含 IL 代码和元数据
  • 运行时编译:CLR 通过 JIT 将 IL 编译为本地机器码执行
这里我们只需要了解大概流程就好了,至于里面是否有再细节一点的流程,甚至 AOT/JIT,就不去深究,后面有机会再分享,属于另外一范畴,可以看到这里就出现了 Roslyn,他的作用就是用于编译原生的 C# 代码为 IL,你可以把他理解为是一个开源编译器平台,而且他本身还是用 C# 写的,相信自己的直觉,没错,用 C# 写的代码编译 C# ,俗称自举,约等于(鸡生蛋、蛋生鸡),形成这种局面开始是在微软诞生了 Roslyn 之后,早期的编译器还是用 C++ 的。
2. 常见问题

Q1:Roslyn 可以编译其他代码吗?如果能编译我自己可以设计一个语言,来用 Roslyn 来编译吗?还是只能编译 C# 和 VB.NET 吗?


  • 其实 Roslyn 只能编译 C# 和 VB.NET,如果咱们使用定义一个 X 语言,也不能用 Roslyn 来编译,除非以 Roslyn 作为参考,自己写解析器。
Q2:他是怎么编译的竟然可以把 C# 代码编译为 IL 代码,Roslyn 编译流程?


  • 语法分析(Parsing) → 生成 Syntax Tree(语法树)
  • 语义分析(Semantic Analysis) → 生成 Symbols 和 Bindings
  • IL 生成(Code Generation) → Emit IL
2. Roslyn 有哪些应用

上面解释了他可以作为编译器来编译 C# 代码,当然他作为一个开源平台 他的作用远不止这些,不过在这里只做一些简单的介绍和示例,后续会单独发布文章做一些分享,下面介绍一下:
功能


  • 语法树(Syntax Tree):解析源代码为一个结构化的表示形式。
  • 语义模型(Semantic Model):提供对代码中符号及其含义的理解。
  • 诊断(Diagnostics):允许开发创建自定义的编译时检查规则。
  • 重构工具:支持开发代码重构工具,如自动修复、代码清理等。
  • 代码生成:可以用来生成新的代码文件或修改现有的代码。
应用场景


  • 开发 Visual Studio 扩展插件。
  • 创建静态分析工具,例如流行的 SonarLint、ReSharper、GitHub Code Scanning。
  • 实现代码质量检查工具,例如检测代码中是否有一些开发团队不允许的代码,循环调用数据库等。
  • 构建代码生成工具,使用源生成器在编译阶段编译通用代码。
  • 动态编译执行代码,在程序运行时,让用户输入一段 C# 代码字符串,然后立即编译并执行。当然有大佬封装了一个库,natasha
是不是看了之后很惊讶,甚至有可能之前觉得不可能,甚至不知道怎么实现的技术,似乎找到了一些眉目,其实他的强大在于他能拿到你源代码的语法树,进行语法分析,语义分析,如果您搞不清语法和语义分析是什么意思,看下面的例子,我尽可能的讲清楚。
3. 语法分析

下面定义一个 C# 代码,其实在编译时它们是被读取为字符串的,因为编译时 Roslyn 肯定是将代码都是作为文件然后读取字符串的,不然怎么解析呢?
字符串中包含 5 个 using 引用,一个类型声明,2 个方法,1 个带参数,空返回值,一个不带参数,空返回值。
  1. using System;
  2. using System.Collections;
  3. using System.Linq;
  4. using System.Text;
  5. using Microsoft.CodeAnalysis;
  6. namespace HelloWorld
  7. {
  8.     class Program
  9.     {
  10.         static void Main(string[] args)
  11.         {
  12.             Console.WriteLine("Hello World!");
  13.         }
  14.         static void Main1()
  15.         {
  16.             Console.WriteLine("HelloMain1!");
  17.         }
  18.     }
  19. }
复制代码
然后我们使用 VS 打开这个代码,用可视化语法树工具查看,你就能理解为什么叫语法树分析,左边是源代码,右边就是工具分析出的这个源代码的语法树结构,第一层根节点叫【CompilationUnit】又叫编译单元,相当于一个文件就是一个单独的编译单元,而且呈现树形生长,你把鼠标移到对应的源代码元素上,都会在右侧可视化工具中找到对应的树节点。

这里面有不同的颜色标记的都叫做一个语法:

  • 语法节点(SyntaxNode) 被标记为蓝色,例如方法、类、表达式等,
  • 语法标记(SyntaxToken) 被标记为绿色,例如关键字 static、void ,VoidKeyword 就代表空返回值。
  • 语法杂项(SyntaxTrivia) 被标记为红色,例如一些空格注释
Syntax 语法 API

Syntax类型用于表示源代码的语法结构,是构建和操作 C# 代码抽象语法树(AST)的基础。
一般语法树从大到小:

  • using 指令 - UsingDirectiveSyntax
  • 成员定义的语法 - MemberDeclarationSyntax每一个 node 都包含有 MemberDeclarationSyntax
  • 命名空间语法 - NamespaceDeclarationSyntax
  • 类定义语法 - ClassDeclarationSyntax
  • 方法定义语法 - MethodDeclarationSyntax
  • 参数定义语法 - ParameterSyntax
可以访问这个网站:https://roslynquoter.azurewebsites.net/
然后把代码粘贴进去点击生成就会出现以下内容:

我们思考下,我们都拿到了代码的逻辑语法结构,是不是找什么就容易了,因为源代码的每一个字符,每一个代码都对应一个语法标记,现在知道为什么我们使用 VS 开发代码时,有时候没写括号或者少了标点,就会提示错误了吧?其实就是实时在检测您写的代码的语法树,是不是符合规则,如果不符合就产生对应的错误。
看着头疼,如果不能理解可以指出。读不懂没有关系,因为这一步是主要说明什么是语法树和语法结构,可以判断你的结构对不对,那如何判断内容和意义对不对呢?接着往下看!
4. 语义分析

例如我一个方法返回值是 Int ,我返回一个 string
  1. static int Main2()
  2. {
  3.     return "1";
  4. }
复制代码
再分别看可视化的语法树也正常长出来了,在线的分析工具也能分析出来。


但是我们作为开发人员肯定知道,我要 INT 你返回 string,能用才怪呢,逻辑就对不上,在 VS 中飘红是因为他有检测,如果你用记事本写,是不是没啥问题,符合 C# 语法,但它没有足够的信息来标识所引用的内容是什么意思。因为名称可能表示一种类型,方法,局部变量,语义不一样,这个时候就要说另外一个东西了,就是 语义分析,就是我解析生成了语法结构,我还得知道每个节点代表什么意思他的意义是什么。只有知道了语义之后才能真正"活"起来。
5. 利用 Roslyn API 进行语法以及语义分析

先定义代码字符串,因为在编译时 Roslyn 就是将源代码文件作为字符串读取,形成上面描述那样的语法树逻辑结构。
  1. public  const string ProgramText =
  2.    @"using System;
  3.        using System.Collections;
  4.        using System.Linq;
  5.        using System.Text;
  6.        using Microsoft.CodeAnalysis;
  7.        namespace HelloWorld
  8.        {
  9.            class Program
  10.            {
  11.                static void Main(string[] args)
  12.                {
  13.                    Console.WriteLine(""Hello, World!"");
  14.                }
  15.                static void Main1()
  16.                {      
  17.                       var list= new List<string>() { ""21""};
  18.                        list.Add(""c"");
  19.                    Console.WriteLine(""Hello, Main1!"");
  20.                }
  21.            }
  22.        }";
复制代码
1. 语法分析

直接从语法节点获取返回类型 && 使用语法树分析遍历每个节点
  1. static void Main(string[] args)
  2. {
  3.     SyntaxTree tree = CSharpSyntaxTree.ParseText(ProgramText);
  4.     CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
  5.    
  6.     WriteLine($"语法树有 {root.Members.Count} 个元素在里面.");
  7.     WriteLine($"这个语法树有 {root.Usings.Count} using 语句,分别是:");
  8.     foreach (UsingDirectiveSyntax element in root.Usings)
  9.         WriteLine($"\t{element.Name}");
  10.     MemberDeclarationSyntax firstMember = root.Members[0];
  11.     WriteLine($"第一个成员是: {firstMember.Kind()}.");
  12.     var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;
  13.     WriteLine($"命名空间{helloWorldDeclaration.Name}下声明了 {helloWorldDeclaration.Members.Count} 个成员.");
  14.     WriteLine($"第一个成员的类型是: {helloWorldDeclaration.Members[0].Kind()}.");
  15.     var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
  16.     WriteLine($"有 {programDeclaration.Members.Count} 个成员定义在 {programDeclaration.Identifier} 类中.");
  17.     //直接从语法节点获取返回类型
  18.     for (int i = 0; i < programDeclaration.Members.Count; i++)
  19.     {
  20.         WriteLine($"第{i+1}个成员是一个 {programDeclaration.Members[i].Kind()}类型.");
  21.         var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[i];
  22.         WriteLine($" {mainDeclaration.Identifier} :方法的返回类型是: {mainDeclaration.ReturnType}.");
  23.         WriteLine($"方法有: {mainDeclaration.ParameterList.Parameters.Count} 个参数.");
  24.         foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
  25.             WriteLine($"{item.Identifier} 参数的类型是: {item.Type}.");
  26.         WriteLine($"{mainDeclaration.Identifier} 方法体内容如下:");
  27.         WriteLine(mainDeclaration.Body.ToFullString());
  28.         if (mainDeclaration.ParameterList.Parameters.Any())
  29.         {
  30.             var argsParameter = mainDeclaration.ParameterList.Parameters[0];
  31.             var firstParameters = from methodDeclaration in root.DescendantNodes()
  32.                                                     .OfType<MethodDeclarationSyntax>()
  33.                                   where methodDeclaration.Identifier.ValueText == "Main"
  34.                                   select methodDeclaration.ParameterList.Parameters.First();
  35.             var argsParameter2 = firstParameters.Single();
  36.             WriteLine(argsParameter == argsParameter2);
  37.         }
  38.     }
  39. }
复制代码
输出
  1. 语法树有 1 个元素在里面.
  2. 这个语法树有 5 using 语句,分别是:
  3.         System
  4.         System.Collections
  5.         System.Linq
  6.         System.Text
  7.         Microsoft.CodeAnalysis
  8.         
  9. 第一个成员是: NamespaceDeclaration.
  10. 命名空间HelloWorld下声明了 1 个成员.
  11. 第一个成员的类型是: ClassDeclaration.
  12. 有 2 个成员定义在 Program 类中.
  13. 第1个成员是一个 MethodDeclaration类型.
  14.      Main :方法的返回类型是: void.
  15.         方法有: 1 个参数.
  16.             args 参数的类型是: string[].
  17.                 Main 方法体内容如下:
  18.                     {
  19.                         Console.WriteLine("Hello, World!");
  20.                     }
  21. 第2个成员是一个 MethodDeclaration类型.
  22.      Main1 :方法的返回类型是: void.
  23.         方法有: 0 个参数.
  24.             Main1 方法体内容如下:
  25.                 {
  26.                     Console.WriteLine("Hello, Main1!");
  27.                 }
复制代码
是不是感觉理解了一些,接着看语法分析可以拿到你的代码块中你想关注的更多有用的信息。
2. 语义分析

接下来我们开始进行语义分析,说白了就是:在语法结构正确的基础上,搞清楚这段代码到底要干什么。它会顺着语法树,一层层看懂每个部分的真正含义,比如变量是谁、函数怎么用、类型对不对,最后把程序的‘真实意图’给挖出来。
  1. // 为 programText 常量中的代码文本生成语法树
  2. SyntaxTree tree = CSharpSyntaxTree.ParseText(ProgramText);
  3. CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
  4. var compilation = CSharpCompilation.Create("HelloWorld")
  5.          .AddReferences(MetadataReference.CreateFromFile(
  6.              typeof(string).Assembly.Location))
  7.          .AddSyntaxTrees(tree);
  8.       
  9. var methods2 = (from methodDeclaration in root.DescendantNodes()
  10.                                                      .OfType<MethodDeclarationSyntax>()
  11.                 select methodDeclaration).ToList();
  12.       
  13. //遍历语法节点 ,找到所有的方法定义
  14. foreach (var item in methods2)
  15. {
  16.     //获取整个语义模型
  17.     var model = compilation.GetSemanticModel(tree);
  18.     //根据当前语法节点,利用语义模型找到当前方法的符号
  19.     var semanticMethod = model.GetDeclaredSymbol(item);
  20.      
  21.     //获取当前方法的返回值
  22.     Console.WriteLine(semanticMethod.ReturnType);
  23. }
复制代码
其实语义分析的重点就是 compilation.GetSemanticModel(tree),他的作用就是得到一个语义模型,然后通过它可以查询出,当前分析的这段代码的意思,他在这个范围内的名称是什么,他可以访问哪些成员,定义了哪些变量。
如果您还没体会到好处,可能不太深刻,可以按照例子自己试试。
6. 扩展

看了上面的语法和语义分析,您可能还是有点懵,说了一大堆,拿到了有啥用,看了跟没看一样,他能做什么,不过不要紧,在这里我尽可能的让您知道他的好处。

  • 相信您只要做了开发,一定对在代码中提交事务不会陌生吧,在团队开发中,曾几何时是否有忘记过写 Commit() 然后发现一顿操作无效


  • 在团队开发中,有些人的代码总是不合要求,让入参小写,非要大写,方法名让大写,他小写,等到一段时间之后,代码看着痛苦不堪,又或者上线后因为异步方法使用 void 来作为方法返回值,产生了莫名其妙的异常,查了半天,还没搞定,最后回滚代码。

但是作为技术 leader 或者高级开发的你精力有限,不可能每个人我都去盯着吧,就这样不知所措....
此时你想要是能在一开始就不让写这样的代码不就好了,就像我在 VS 写的时候直接飘红,那我怎么弄呢,恭喜您已经入门了,这时候就可以自己定义一个代码分析器来检查这些问题,上面已经提到 Roslyn 的一个重要应用场景就是代码分析,Roslyn 的特点和作用,我们在语法和语义分析部分已经大概了解,联想一下,是不是若有所思,思路如下:

  • 通过 roslyn 解析我的代码,然后解析出语法结构和语义模型
  • 根据语法树,我找到所有的方法节点,然后通过语义解析出,找到所有的 Task 方法,将符合并且返回值是 void 直接调出来是不是就可以了
但是也不要被此局限,因为他提供的远不止我简单描述的做这些。
7. 用 Roslyn 打造代码规约和动态编译

1. 用 Roslyn 构建代码规范检查器,禁止 async void 方法

① 创建自定义分析器

我们定义一个类 UserDiagnosticAnalyzer,继承自 DiagnosticAnalyzer,并标注 [DiagnosticAnalyzer(LanguageNames.CSharp)],表明这是一个针对 C# 语言的语法分析器。
  1. [DiagnosticAnalyzer(LanguageNames.CSharp)]
  2. public class UserDiagnosticAnalyzer : DiagnosticAnalyzer
  3. {
  4.     // ...
  5. }
复制代码
② 定义诊断规则

通过 DiagnosticDescriptor定义一条诊断规则,当检测到违规代码时,输出一条错误信息,例如“异步方法不能返回 void”。
  1. private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
  2.     id: "Code001",
  3.     title: "示例规则标题",
  4.     messageFormat: "代码规范检查:{0}",
  5.     category: "Usage",
  6.     defaultSeverity: DiagnosticSeverity.Error,
  7.     isEnabledByDefault: true
  8. );
复制代码
③ 注册分析逻辑

在 Initialize方法中,我们注册一个语法节点分析动作,监听所有 方法声明(MethodDeclaration) 节点:
  1. public override void Initialize(AnalysisContext context)
  2. {
  3.     context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
  4.     context.EnableConcurrentExecution();
  5.     context.RegisterSyntaxNodeAction(AnalyzeSymbolAnalysisContext, SyntaxKind.MethodDeclaration);
  6. }
复制代码

  • EnableConcurrentExecution()提升分析性能;
  • RegisterSyntaxNodeAction指定当解析到方法声明时,调用 AnalyzeSymbolAnalysisContext进行分析。
④ 实现分析逻辑

在回调方法中,我们判断方法是否同时满足两个条件:

  • 包含 async关键字;
  • 返回类型为 void。
  1. private static void AnalyzeSymbolAnalysisContext(SyntaxNodeAnalysisContext context)
  2. {
  3.     if (context.Node is MethodDeclarationSyntax method)
  4.     {
  5.         if (method.Modifiers.Any(x => x.IsKind(SyntaxKind.AsyncKeyword))
  6.             && method.ReturnType.ToString() == "void")
  7.         {
  8.             var diagnostic = Diagnostic.Create(
  9.                 Rule,
  10.                 context.Node.GetLocation(),
  11.                 "异步方法不能返回void"
  12.             );
  13.             context.ReportDiagnostic(diagnostic);
  14.         }
  15.     }
  16. }
复制代码
然后在业务代码中引用,当你写这种不符合规范的代码,IDE 会立即在代码下方显示红色波浪线,就会提示错误。

2. 使用 Roslyn 来动态编译代码

它的另外一个作用就是 动态编译,Roslyn 不仅仅是一个编译器平台,它还提供了强大的动态编译与代码执行能力,这一特性在构建可扩展的中后台系统时很实用。
举个典型的场景:我们有一个底层通用功能平台(比如审批流程、数据校验、报表生成等),多个业务系统都基于这个平台进行开发。虽然核心逻辑是通用的,但每个业务方可能需要在标准流程中插入自定义逻辑,比如在某个方法执行前后修改数据、记录日志、调用特定服务等。
传统做法是通过接口 + 插件模式或依赖注入来实现扩展,但这要求编译期就确定实现类,不够灵活。而借助 Roslyn 的动态编译能力,我们可以让业务开发人员以脚本形式编写扩展逻辑,在运行时动态编译并执行,真正做到热插拔式的定制。
我们定义一个通用的数据处理流程,在关键节点允许业务方传入一段 C# 脚本:
  1. var script = @"parameters.Value += 1;";
  2. var action = RoslynScriptRunner.CreateScript(script);
  3. AA aA1 = new AA();
  4. aA1.Value = 99;
  5. action.Invoke(aA1);
  6. Console.WriteLine(aA1.Value); // 输出 100
复制代码
在这个例子中:

  • AA是我们约定的数据上下文对象。
  • script是由业务方提供的 C# 表达式脚本,表示对 Value加 1。
  • RoslynScriptRunner是封装了 Roslyn 编译和执行逻辑的工具类。
  • 在运行时,平台动态编译这段脚本,并将业务对象 aA1作为参数传入执行。
这样一来,不同业务系统可以在不修改主流程代码的前提下,灵活注入自己的逻辑,实现真正的运行时扩展。
这种模式特别适用于:

  • 需要频繁变更的业务规则;
  • 多租户系统中的个性化定制;
  • 平台化产品中开放二次开发能力;
通过 Roslyn,我们把“代码”当作“配置”来管理,提升系统的灵活性,后续会专门再开文章分享,关于Roslyn的另外应用场景和进阶使用,包括但不限于如何实现运行时编译执行和源生成器。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册