2026/2/16 17:43:49
网站建设
项目流程
短期网站建设培训,在郑州建设网站这么做,网站备案审批号,聊城做网站最好的网络公司1.什么是 Roslyn聊起 Roslyn 可能对于有部分小伙伴有些陌生#xff0c;有些小伙听过但是没接触过#xff0c;有些小伙伴可能比较擅长#xff0c;其实在这之前我也是个懵的#xff0c;听过但是没深入了解#xff0c;因为我不知道并不影响我做一些增删改查#xff0c;但是如…1.什么是 Roslyn聊起 Roslyn 可能对于有部分小伙伴有些陌生有些小伙听过但是没接触过有些小伙伴可能比较擅长其实在这之前我也是个懵的听过但是没深入了解因为我不知道并不影响我做一些增删改查但是如果你要深入或者写一些框架底层或者提升效率的工具以及扩展那这个是必须掌握的技术。年初时我在与技术大牛 痴者工良 交流的过程中算是正式接触到 Roslyn瞬间被它的强大能力所吸引。他深入浅出的讲解让我意识到这不仅是编译器黑科技更是提升代码质量与开发效率的利器。受他启发我开始系统学习虽断断续续折腾了一阵但一直未做总结。最近终于得空便将所学梳理成文分享出来既是记录也是致敬好朋友严架的帮助。在正式认识 Roslyn 之前我们必须先对咱们 C# .NET 的编译流程有个大概了解当然 VB.NET 也适用但是接受不来他的语法有些小伙伴可能知道或者了解简单的给个图感受一下。image1. C# / .NET 编译流程简述源代码阶段我们手动写出 C# 或者 VB.NET 代码编译器阶段Roslyn 编译器将源代码转换为 ILIntermediate Language中间代码IL 生成生成 .dll 或 .exe 文件包含 IL 代码和元数据运行时编译CLR 通过 JIT 将 IL 编译为本地机器码执行这里我们只需要了解大概流程就好了至于里面是否有再细节一点的流程甚至 AOT/JIT就不去深究后面有机会再分享属于另外一范畴可以看到这里就出现了 Roslyn他的作用就是用于编译原生的 C# 代码为 IL你可以把他理解为是一个开源编译器平台而且他本身还是用 C# 写的相信自己的直觉没错用 C# 写的代码编译 C# 俗称自举约等于鸡生蛋、蛋生鸡形成这种局面开始是在微软诞生了 Roslyn 之后早期的编译器还是用 C 的。2. 常见问题Q1Roslyn 可以编译其他代码吗如果能编译我自己可以设计一个语言来用 Roslyn 来编译吗还是只能编译 C# 和 VB.NET 吗其实 Roslyn 只能编译 C# 和 VB.NET如果咱们使用定义一个 X 语言也不能用 Roslyn 来编译除非以 Roslyn 作为参考自己写解析器。Q2他是怎么编译的竟然可以把 C# 代码编译为 IL 代码Roslyn 编译流程语法分析Parsing → 生成 Syntax Tree语法树语义分析Semantic Analysis → 生成 Symbols 和 BindingsIL 生成Code Generation → Emit IL2. 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】又叫编译单元相当于一个文件就是一个单独的编译单元而且呈现树形生长你把鼠标移到对应的源代码元素上都会在右侧可视化工具中找到对应的树节点。image这里面有不同的颜色标记的都叫做一个语法语法节点SyntaxNode 被标记为蓝色例如方法、类、表达式等语法标记SyntaxToken 被标记为绿色例如关键字 static、void VoidKeyword 就代表空返回值。语法杂项SyntaxTrivia 被标记为红色例如一些空格注释Syntax 语法 APISyntax类型用于表示源代码的语法结构是构建和操作 C# 代码抽象语法树AST的基础。一般语法树从大到小using 指令 - UsingDirectiveSyntax成员定义的语法 - MemberDeclarationSyntax每一个 node 都包含有 MemberDeclarationSyntax命名空间语法 - NamespaceDeclarationSyntax类定义语法 - ClassDeclarationSyntax方法定义语法 - MethodDeclarationSyntax参数定义语法 - ParameterSyntax可以访问这个网站https://roslynquoter.azurewebsites.net/然后把代码粘贴进去点击生成就会出现以下内容image我们思考下我们都拿到了代码的逻辑语法结构是不是找什么就容易了因为源代码的每一个字符每一个代码都对应一个语法标记现在知道为什么我们使用 VS 开发代码时有时候没写括号或者少了标点就会提示错误了吧其实就是实时在检测您写的代码的语法树是不是符合规则如果不符合就产生对应的错误。看着头疼如果不能理解可以指出。读不懂没有关系因为这一步是主要说明什么是语法树和语法结构可以判断你的结构对不对那如何判断内容和意义对不对呢接着往下看4. 语义分析例如我一个方法返回值是 Int 我返回一个 stringstatic int Main2(){return 1;}再分别看可视化的语法树也正常长出来了在线的分析工具也能分析出来。imageimage但是我们作为开发人员肯定知道我要 INT 你返回 string能用才怪呢逻辑就对不上在 VS 中飘红是因为他有检测如果你用记事本写是不是没啥问题符合 C# 语法但它没有足够的信息来标识所引用的内容是什么意思。因为名称可能表示一种类型方法局部变量语义不一样这个时候就要说另外一个东西了就是 语义分析就是我解析生成了语法结构我还得知道每个节点代表什么意思他的意义是什么。只有知道了语义之后才能真正活起来。5. 利用 Roslyn API 进行语法以及语义分析先定义代码字符串因为在编译时 Roslyn 就是将源代码文件作为字符串读取形成上面描述那样的语法树逻辑结构。public const 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 Liststring() { 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[0];WriteLine($第一个成员是 {firstMember.Kind()}.);var helloWorldDeclaration (NamespaceDeclarationSyntax)firstMember;WriteLine($命名空间{helloWorldDeclaration.Name}下声明了 {helloWorldDeclaration.Members.Count} 个成员.);WriteLine($第一个成员的类型是 {helloWorldDeclaration.Members[0].Kind()}.);var programDeclaration (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];WriteLine($有 {programDeclaration.Members.Count} 个成员定义在 {programDeclaration.Identifier} 类中.);//直接从语法节点获取返回类型for (int i 0; i programDeclaration.Members.Count; i){WriteLine($第{i1}个成员是一个 {programDeclaration.Members[i].Kind()}类型.);var mainDeclaration (MethodDeclarationSyntax)programDeclaration.Members[i];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[0];var firstParameters from methodDeclaration in root.DescendantNodes().OfTypeMethodDeclarationSyntax()where methodDeclaration.Identifier.ValueText Mainselect methodDeclaration.ParameterList.Parameters.First();var argsParameter2 firstParameters.Single();WriteLine(argsParameter argsParameter2);}}}输出语法树有 1 个元素在里面.这个语法树有 5 using 语句分别是:SystemSystem.CollectionsSystem.LinqSystem.TextMicrosoft.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().OfTypeMethodDeclarationSyntax()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() 然后发现一顿操作无效image在团队开发中有些人的代码总是不合要求让入参小写非要大写方法名让大写他小写等到一段时间之后代码看着痛苦不堪又或者上线后因为异步方法使用 void 来作为方法返回值产生了莫名其妙的异常查了半天还没搞定最后回滚代码。image但是作为技术 leader 或者高级开发的你精力有限不可能每个人我都去盯着吧就这样不知所措....此时你想要是能在一开始就不让写这样的代码不就好了就像我在 VS 写的时候直接飘红那我怎么弄呢恭喜您已经入门了这时候就可以自己定义一个代码分析器来检查这些问题上面已经提到 Roslyn 的一个重要应用场景就是代码分析Roslyn 的特点和作用我们在语法和语义分析部分已经大概了解联想一下是不是若有所思思路如下通过 roslyn 解析我的代码然后解析出语法结构和语义模型根据语法树我找到所有的方法节点然后通过语义解析出找到所有的 Task 方法将符合并且返回值是 void 直接调出来是不是就可以了但是也不要被此局限因为他提供的远不止我简单描述的做这些。7. 用 Roslyn 打造代码规约和动态编译1. 用 Roslyn 构建代码规范检查器禁止 async void 方法① 创建自定义分析器我们定义一个类 UserDiagnosticAnalyzer继承自 DiagnosticAnalyzer并标注 [DiagnosticAnalyzer(LanguageNames.CSharp)]表明这是一个针对 C# 语言的语法分析器。[DiagnosticAnalyzer(LanguageNames.CSharp)]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 会立即在代码下方显示红色波浪线就会提示错误。image2. 使用 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的另外应用场景和进阶使用包括但不限于如何实现运行时编译执行和源生成器。