辖瑁地 发表于 2025-10-15 21:20:06

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

1.什么是 Roslyn

聊起 Roslyn 可能对于有部分小伙伴有些陌生,有些小伙听过但是没接触过,有些小伙伴可能比较擅长,其实在这之前我也是个懵的,听过但是没深入了解,因为我不知道并不影响我做一些增删改查,但是如果你要深入,或者写一些框架底层或者提升效率的工具以及扩展,那这个是必须掌握的技术。
年初时,我在与技术大牛 痴者工良交流的过程中,算是正式接触到 Roslyn,瞬间被它的强大能力所吸引。他深入浅出的讲解让我意识到,这不仅是编译器黑科技,更是提升代码质量与开发效率的利器。受他启发,我开始系统学习,虽断断续续折腾了一阵,但一直未做总结。最近终于得空,便将所学梳理成文,分享出来,既是记录,也是致敬好朋友严架的帮助。
在正式认识 Roslyn 之前,我们必须先对咱们 C# .NET 的编译流程有个大概了解,当然 VB.NET 也适用,但是接受不来他的语法,有些小伙伴可能知道或者了解,简单的给个图感受一下。
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=ZDhmZmY0OWQ5NmE4MDQ1NDU1YzNmODk5N2IxZDA3NmFfTjJXY2kyZVZOWVhuRkExMFZNdzZlcERMYk9mTXdSVW9fVG9rZW46UE5OcWJKVFNVbzJhOGR4WGQ1R2NsYWlJbllnXzE3NjA0OTkxNzA6MTc2MDUwMjc3MF9WNA
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 个带参数,空返回值,一个不带参数,空返回值。
using System;
using System.Collections;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace HelloWorld
{
    class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("Hello World!");
      }

      static void Main1()
      {
            Console.WriteLine("HelloMain1!");
      }
    }
}然后我们使用 VS 打开这个代码,用可视化语法树工具查看,你就能理解为什么叫语法树分析,左边是源代码,右边就是工具分析出的这个源代码的语法树结构,第一层根节点叫【CompilationUnit】又叫编译单元,相当于一个文件就是一个单独的编译单元,而且呈现树形生长,你把鼠标移到对应的源代码元素上,都会在右侧可视化工具中找到对应的树节点。
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=Y2E0MjkxYjYzNzU0ZWVhYzI5ZmY5ZWRjNGQ4NTUzMGFfMnlwUDNmaTVacFYzbmZkNFk0SUpRZzVFRVd6T1RZc1ZfVG9rZW46T2pqOGJKSTB5b3Q5SEx4dzZuZWNxblF1bnljXzE3NjA0OTkxODU6MTc2MDUwMjc4NV9WNA
这里面有不同的颜色标记的都叫做一个语法:

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

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

[*]using 指令 - UsingDirectiveSyntax
[*]成员定义的语法 - MemberDeclarationSyntax每一个 node 都包含有 MemberDeclarationSyntax
[*]命名空间语法 - NamespaceDeclarationSyntax
[*]类定义语法 - ClassDeclarationSyntax
[*]方法定义语法 - MethodDeclarationSyntax
[*]参数定义语法 - ParameterSyntax
可以访问这个网站:https://roslynquoter.azurewebsites.net/
然后把代码粘贴进去点击生成就会出现以下内容:
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=YzFkODM2YzNkNzk4OTQxOTM1OGJlMDAwZGQ5NGUwZDNfWjhrcmJGSVc1dUcxZ3UzYU5jcEpxc2FWdVQyMlpudUhfVG9rZW46WUJ0QmJKZlVIb0lzdjl4RkVFR2NXS0pabkVMXzE3NjA0OTkyMDY6MTc2MDUwMjgwNl9WNA
我们思考下,我们都拿到了代码的逻辑语法结构,是不是找什么就容易了,因为源代码的每一个字符,每一个代码都对应一个语法标记,现在知道为什么我们使用 VS 开发代码时,有时候没写括号或者少了标点,就会提示错误了吧?其实就是实时在检测您写的代码的语法树,是不是符合规则,如果不符合就产生对应的错误。
看着头疼,如果不能理解可以指出。读不懂没有关系,因为这一步是主要说明什么是语法树和语法结构,可以判断你的结构对不对,那如何判断内容和意义对不对呢?接着往下看!
4. 语义分析

例如我一个方法返回值是 Int ,我返回一个 string
static int Main2()
{
    return "1";
}再分别看可视化的语法树也正常长出来了,在线的分析工具也能分析出来。
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=ZGQ3YjA1N2ZkYmE2NWI2ODZlNWVjZDBjYjE2ZjI5OTlfU3lTNTJEb0gzYjZDUXFYQTVpanpHQ3phVkVlWWx3T3NfVG9rZW46RERlamJJVmFEb0xFMTB4RExFU2NhaU5LbmtkXzE3NjA0OTkyMTQ6MTc2MDUwMjgxNF9WNA
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=MmMzMjdmOTAxZjZmMjBhNmI0NTQ1ZDMzZTg3MWIzYTJfbjlndVFpV01peExHbWdadGNHR1JDeW1zdUtjaUdxMnlfVG9rZW46WFhINGJjQUpFb0Nsckt4VFI5N2Nya3Y1bkpoXzE3NjA0OTkyMjY6MTc2MDUwMjgyNl9WNA
但是我们作为开发人员肯定知道,我要 INT 你返回 string,能用才怪呢,逻辑就对不上,在 VS 中飘红是因为他有检测,如果你用记事本写,是不是没啥问题,符合 C# 语法,但它没有足够的信息来标识所引用的内容是什么意思。因为名称可能表示一种类型,方法,局部变量,语义不一样,这个时候就要说另外一个东西了,就是 语义分析,就是我解析生成了语法结构,我还得知道每个节点代表什么意思他的意义是什么。只有知道了语义之后才能真正"活"起来。
5. 利用 Roslyn API 进行语法以及语义分析

先定义代码字符串,因为在编译时 Roslyn 就是将源代码文件作为字符串读取,形成上面描述那样的语法树逻辑结构。
publicconst string ProgramText =
   @"using System;
       using System.Collections;
       using System.Linq;
       using System.Text;
       using Microsoft.CodeAnalysis;
       namespace HelloWorld
       {
         class Program
         {
               static void Main(string[] args)
               {
                   Console.WriteLine(""Hello, World!"");
               }

               static void Main1()
               {      
                      var list= new List<string>() { ""21""};
                     list.Add(""c"");
                   Console.WriteLine(""Hello, Main1!"");
               }
         }
       }";1. 语法分析

直接从语法节点获取返回类型 && 使用语法树分析遍历每个节点

static void Main(string[] args)
{
    SyntaxTree tree = CSharpSyntaxTree.ParseText(ProgramText);
    CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
   
    WriteLine($"语法树有 {root.Members.Count} 个元素在里面.");
    WriteLine($"这个语法树有 {root.Usings.Count} using 语句,分别是:");
    foreach (UsingDirectiveSyntax element in root.Usings)
      WriteLine($"\t{element.Name}");

    MemberDeclarationSyntax firstMember = root.Members;
    WriteLine($"第一个成员是: {firstMember.Kind()}.");
    var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

    WriteLine($"命名空间{helloWorldDeclaration.Name}下声明了 {helloWorldDeclaration.Members.Count} 个成员.");
    WriteLine($"第一个成员的类型是: {helloWorldDeclaration.Members.Kind()}.");

    var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members;
    WriteLine($"有 {programDeclaration.Members.Count} 个成员定义在 {programDeclaration.Identifier} 类中.");

    //直接从语法节点获取返回类型
    for (int i = 0; i < programDeclaration.Members.Count; i++)
    {
      WriteLine($"第{i+1}个成员是一个 {programDeclaration.Members.Kind()}类型.");
      var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members;

      WriteLine($" {mainDeclaration.Identifier} :方法的返回类型是: {mainDeclaration.ReturnType}.");
      WriteLine($"方法有: {mainDeclaration.ParameterList.Parameters.Count} 个参数.");
      foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
            WriteLine($"{item.Identifier} 参数的类型是: {item.Type}.");
      WriteLine($"{mainDeclaration.Identifier} 方法体内容如下:");
      WriteLine(mainDeclaration.Body.ToFullString());

      if (mainDeclaration.ParameterList.Parameters.Any())
      {
            var argsParameter = mainDeclaration.ParameterList.Parameters;
            var firstParameters = from methodDeclaration in root.DescendantNodes()
                                                    .OfType<MethodDeclarationSyntax>()
                                  where methodDeclaration.Identifier.ValueText == "Main"
                                  select methodDeclaration.ParameterList.Parameters.First();

            var argsParameter2 = firstParameters.Single();

            WriteLine(argsParameter == argsParameter2);
      }

    }
}输出

语法树有 1 个元素在里面.
这个语法树有 5 using 语句,分别是:
      System
      System.Collections
      System.Linq
      System.Text
      Microsoft.CodeAnalysis
      
第一个成员是: NamespaceDeclaration.
命名空间HelloWorld下声明了 1 个成员.
第一个成员的类型是: ClassDeclaration.

有 2 个成员定义在 Program 类中.
第1个成员是一个 MethodDeclaration类型.
   Main :方法的返回类型是: void.
      方法有: 1 个参数.
            args 参数的类型是: string[].
                Main 方法体内容如下:
                  {
                        Console.WriteLine("Hello, World!");
                  }

第2个成员是一个 MethodDeclaration类型.
   Main1 :方法的返回类型是: void.
      方法有: 0 个参数.
            Main1 方法体内容如下:
                {
                  Console.WriteLine("Hello, Main1!");
                }是不是感觉理解了一些,接着看语法分析可以拿到你的代码块中你想关注的更多有用的信息。
2. 语义分析

接下来我们开始进行语义分析,说白了就是:在语法结构正确的基础上,搞清楚这段代码到底要干什么。它会顺着语法树,一层层看懂每个部分的真正含义,比如变量是谁、函数怎么用、类型对不对,最后把程序的‘真实意图’给挖出来。
// 为 programText 常量中的代码文本生成语法树
SyntaxTree tree = CSharpSyntaxTree.ParseText(ProgramText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

var compilation = CSharpCompilation.Create("HelloWorld")
         .AddReferences(MetadataReference.CreateFromFile(
             typeof(string).Assembly.Location))
         .AddSyntaxTrees(tree);
      
var methods2 = (from methodDeclaration in root.DescendantNodes()
                                                   .OfType<MethodDeclarationSyntax>()
                select methodDeclaration).ToList();
      
//遍历语法节点 ,找到所有的方法定义
foreach (var item in methods2)
{
    //获取整个语义模型
    var model = compilation.GetSemanticModel(tree);

    //根据当前语法节点,利用语义模型找到当前方法的符号
    var semanticMethod = model.GetDeclaredSymbol(item);
   
    //获取当前方法的返回值
    Console.WriteLine(semanticMethod.ReturnType);
}其实语义分析的重点就是 compilation.GetSemanticModel(tree),他的作用就是得到一个语义模型,然后通过它可以查询出,当前分析的这段代码的意思,他在这个范围内的名称是什么,他可以访问哪些成员,定义了哪些变量。
如果您还没体会到好处,可能不太深刻,可以按照例子自己试试。
6. 扩展

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

[*]相信您只要做了开发,一定对在代码中提交事务不会陌生吧,在团队开发中,曾几何时是否有忘记过写 Commit() 然后发现一顿操作无效
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=YTUwYmJhNGFkYWZkNDAzZGM1NzFlZDcwMTZhNWJjMGZfd1JIRjl3SzBQMVhtclA4bmFWWnBidndOY0RPM3R4S1NfVG9rZW46TkZpdWJ6UGpsb3paamR4ZXJlcWN0dktQblJlXzE3NjA0OTkyMzc6MTc2MDUwMjgzN19WNA

[*]在团队开发中,有些人的代码总是不合要求,让入参小写,非要大写,方法名让大写,他小写,等到一段时间之后,代码看着痛苦不堪,又或者上线后因为异步方法使用 void 来作为方法返回值,产生了莫名其妙的异常,查了半天,还没搞定,最后回滚代码。
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=OTJlYTY3YTI1YWUyYmQ4ODU2Mzg4NDM0YTVlNjRlNDhfTHpEZDl4RVVISlFlMlpIUzFVeHN5QU9HcUpyN3RQbzdfVG9rZW46WjVRUWJnUEk5b0pEUXl4bk5pdmNkZlVibmdyXzE3NjA0OTkyNDg6MTc2MDUwMjg0OF9WNA
但是作为技术 leader 或者高级开发的你精力有限,不可能每个人我都去盯着吧,就这样不知所措....
此时你想要是能在一开始就不让写这样的代码不就好了,就像我在 VS 写的时候直接飘红,那我怎么弄呢,恭喜您已经入门了,这时候就可以自己定义一个代码分析器来检查这些问题,上面已经提到 Roslyn 的一个重要应用场景就是代码分析,Roslyn 的特点和作用,我们在语法和语义分析部分已经大概了解,联想一下,是不是若有所思,思路如下:

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

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

① 创建自定义分析器

我们定义一个类 UserDiagnosticAnalyzer,继承自 DiagnosticAnalyzer,并标注 ,表明这是一个针对 C# 语言的语法分析器。

public class UserDiagnosticAnalyzer : DiagnosticAnalyzer
{
    // ...
}② 定义诊断规则

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

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

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

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

[*]包含 async关键字;
[*]返回类型为 void。
private static void AnalyzeSymbolAnalysisContext(SyntaxNodeAnalysisContext context)
{
    if (context.Node is MethodDeclarationSyntax method)
    {
      if (method.Modifiers.Any(x => x.IsKind(SyntaxKind.AsyncKeyword))
            && method.ReturnType.ToString() == "void")
      {
            var diagnostic = Diagnostic.Create(
                Rule,
                context.Node.GetLocation(),
                "异步方法不能返回void"
            );
            context.ReportDiagnostic(diagnostic);
      }
    }
}然后在业务代码中引用,当你写这种不符合规范的代码,IDE 会立即在代码下方显示红色波浪线,就会提示错误。
https://gotofreight.feishu.cn/space/api/box/stream/download/asynccode/?code=OTIzZWNiOTYxN2M5NjhlZDE5NWYwZDY0NmJmNjkzMmJfeEhRaXprOGxTT09xZWY4SzA5TzRCMmRMS2w2Q0tRUzBfVG9rZW46RHE3YWJ2SkFVb0JNUjZ4cHZ2bWNBZHVnbkliXzE3NjA0OTkyNTQ6MTc2MDUwMjg1NF9WNA
2. 使用 Roslyn 来动态编译代码

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

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

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

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

热琢 发表于 2025-11-8 15:24:22

谢谢分享,辛苦了

匣卒 发表于 2025-11-27 04:46:56

谢谢楼主提供!

凉砧掌 发表于 前天 02:30

感谢,下载保存了
页: [1]
查看完整版本: Roslyn 技术解析:如何利用它做代码规范检查与运行时代码生成?​