Roslyn 是什么 —— 从编译器视角读取、修复、生成 C# 代码
· 小村 豪 · .NET, C#, Roslyn, Analyzer, SourceGenerator, 编译器, 静态分析, 代码生成, 现有资产利用
1. 先要弄清楚的前提
想要处理 C# 源代码的场景,其实比想象中更多。比如下面这些工作:
想禁止某个 API 的特定用法
想机械化地找出过时的写法
想收集方法和类的清单
想从整个项目中调查依赖关系
想在编译期生成定型代码
想在构建时对自研库的误用发出警告
想安全地推进大规模替换或迁移
遇到这类需求时,很容易想直接打开 *.cs 文件,用字符串搜索或正则表达式来处理。
但 C# 并不是字符串。下面这两段代码外观相似,但在含义上是完全不同的东西。
Console.WriteLine("Hello");
MyCompany.Logging.Console.WriteLine("Hello");
而且,Console 这个名字有时也会指向别的类型。
using Console = MyCompany.Logging.Console;
Console.WriteLine("Hello");
如果只看字符串,这些看起来都是 Console.WriteLine。但从编译器的角度看,它到底是 System.Console.WriteLine,还是别的类型,只有做完名称解析才能知道。
这时候登场的就是 Roslyn。Roslyn 是一个把 C# 与 Visual Basic 编译器内部持有的信息,以 API 的形式开放给应用程序和工具使用的平台。
简单来说,借助 Roslyn,可以这样处理 C# 代码:
不当作字符串,而是当作语法来读取
不看外观,而是看含义来理解
不局限于单个文件,而是以项目或解决方案的单位来读取
基于读取结果,给出警告、修复建议、生成代码
本文将整理 Roslyn 的整体面貌、Syntax Tree、SemanticModel、Workspace、Analyzer、Source Generator,以及在实务中的应用场景。
另外,本文中出现的代码已作为可构建、可运行的完整示例(处理 Syntax Tree / SemanticModel 的库、对 DateTime.Now 发出警告的 Analyzer、Source Generator、解析整个解决方案的演示、用于确认误报与漏报的单元测试)发布在 GitHub 上。
roslyn-dotnet-compiler-platform - komurasoft-blog-samples (GitHub)
2. Roslyn 是什么
Roslyn 的正式名称是 .NET Compiler Platform。它既是 C# 与 Visual Basic 的编译器实现,同时也是一组用于构建代码分析工具的 API。
过去,编译器往往被当作这样一个「黑盒」来看待。
输入源代码
编译器进行处理
输出 DLL / EXE
开发者通常无法访问编译器内部生成的这些信息。
但实际上,编译器并不只是把文本转换成机器语言或 IL,在编译过程中,它还会生成下面这些信息。
这段文字是一个类声明
这个标识符是一个局部变量
这个方法调用指向某个类型的某个方法
这个表达式的返回值类型是 string
这段代码存在语法错误
这个引用指向汇编 A 中的某个类型
这个 using 实际上并未被使用
Roslyn 把这类信息开放给开发者使用。因此,Roslyn 不只是一个编译器,而是一个用于理解代码的平台。
3. 使用 Roslyn 能做什么
使用 Roslyn,主要可以做到这些事情。
对 C# / VB 做语法分析
对类型和方法做语义分析
获取编译信息
对整个项目或解决方案做分析
编写自定义 Analyzer
编写 Code Fix
编写 Source Generator
编写重构工具
代码生成
代码转换
写得更贴近实务一些,用法大致是这样。
使用了禁用 API 时发出构建警告
列出使用旧 API 的位置
检查 async 方法的命名规则
检测 IDisposable 遗漏处理的情况
引导开发者正确使用自研框架
在编译期生成 DTO 或映射代码
从配置文件或特性生成定型代码
辅助从 .NET Framework 向 .NET 的迁移调研
Roslyn 的重要之处在于,可以在与编译器相同的基础之上,编写「处理 C# 源代码」的逻辑。
如果用正则表达式或自建解析器来读取 C#,很快就会遇到瓶颈。比如,正确处理下面这些元素并不容易。
using alias
扩展方法
partial class
partial method
global using
nullable annotations
泛型类型
重载决议
条件编译
预处理指令
保留注释与空白的改写
Roslyn 提供了按照 C# 语言规范来处理这些元素的 API。
4. Roslyn 把「语法」与「语义」分开来看
理解 Roslyn 时,最先要分开的就是这两个概念。
语法:代码是如何书写的
语义:这段代码指向的是什么
比如看下面这段代码。
Console.WriteLine(message);
从语法角度看,它的结构是这样的。
表达式语句
调用表达式
成员访问表达式
标识符 Console
标识符 WriteLine
参数 message
但仅凭这些还无法知道含义。Console 是哪个类型、WriteLine 是哪个重载、message 是什么类型,光靠语法是判断不出来的。
要从语义角度理解,需要下面这些信息。
using 的状态
引用的汇编
同一项目内的类型定义
对其他项目的引用
类型推断
重载决议
语言版本
nullable 上下文
在 Roslyn 中,这种区别在 API 层面也是分开的。
Syntax Tree : 表示代码的语法结构
SemanticModel : 表示语法所对应的语义
Compilation : 表示编译所需的全部信息
Workspace : 处理解决方案、项目、文档
掌握了这个区别,理解 Roslyn 的整体脉络就会清晰很多。
5. 什么是 Syntax Tree
Syntax Tree(语法树)是表示源代码语法结构的一棵树。比如假设有下面这样一段代码。
class User
{
public string Name { get; set; }
public void Rename(string name)
{
Name = name;
}
}
从 Roslyn 的角度看,这段代码大致是这样的结构。
CompilationUnit
ClassDeclaration: User
PropertyDeclaration: Name
MethodDeclaration: Rename
Parameter: name
Block
ExpressionStatement
AssignmentExpression
Syntax Tree 并不是简单地按行切分文本,而是把类、方法、属性、表达式、语句、参数、运算符等 C# 语法元素整理成的结构。
来看一个简单的例子。
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
var source = """
class User
{
public string Name { get; set; }
public void Rename(string name)
{
Name = name;
}
}
""";
var tree = CSharpSyntaxTree.ParseText(source);
var root = tree.GetCompilationUnitRoot();
var methods = root
.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
Console.WriteLine(method.Identifier.Text);
}
这段代码会在源代码中查找方法声明并输出方法名。在这个例子中,会得到 Rename。
重点在于,这里并不是用字符串搜索去找 void,而是作为 C# 的语法结构去查找「方法声明」。
6. Node、Token、Trivia
在处理 Syntax Tree 时,经常会遇到这三个术语。
SyntaxNode
SyntaxToken
SyntaxTrivia
SyntaxNode
SyntaxNode 是语法上的一个整体单元。比如下面这些。
类声明
方法声明
属性声明
if 语句
for 语句
赋值表达式
调用表达式
lambda 表达式
在 C# 语法中,还能拥有子元素的部分就是 Node。
SyntaxToken
SyntaxToken 是构成语法的最小单位。比如下面这些。
class 关键字
public 关键字
标识符 User
标识符 Rename
{ 和 }
; 和 ,
字符串字面量
数值字面量
Token 是语法树末端的元素。
SyntaxTrivia
SyntaxTrivia 与通常的语义解析没有直接关系,但却是还原源代码所必需的信息。比如下面这些。
空白
换行
注释
预处理指令
正因为有了 Trivia,Roslyn 才能连同注释和空白一起,以很高的还原度处理源代码。
在做代码整形、重构、机械式改写时,Trivia 非常重要。
如果只是想生成一棵 AST,丢弃注释似乎也没问题。但在实务中的代码转换里,不破坏注释和换行是很重要的一点。
7. Syntax Tree 是不可变的
Roslyn 的 Syntax Tree 是不可变(immutable)的。也就是说,不会直接修改已经获取到的语法树,而是生成一棵带有变更的新语法树。
比如想修改方法名时,也不是就地修改现有的 MethodDeclarationSyntax。
var newMethod = oldMethod.WithIdentifier(
SyntaxFactory.Identifier("NewName"));
而是像上面这样,创建一个新的节点。
不可变性带来了一些好处。
更容易在多线程中处理
可以安全地处理 IDE 编辑过程中的快照
更容易生成差异
更容易对比修改前后的内容
刚开始可能会觉得有点繁琐。但在 IDE、构建、Analyzer、Source Generator 这种多个处理会同时引用同一份代码的场景下,不可变性会成为很大的优势。
8. 什么是 SemanticModel
光靠 Syntax Tree,只能知道代码的外观。要理解其含义,需要使用 SemanticModel。
比如考虑下面这段代码。
Console.WriteLine("Hello");
从 Syntax Tree 中,只能知道存在 Console 和 WriteLine 这两个标识符。但它到底指向 System.Console.WriteLine(string?),还是指向别的类型的方法,无法得知。
使用 SemanticModel,就可以获取「这个语法节点被解析成了哪个符号」。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
var source = """
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello");
}
}
""";
var tree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create(
assemblyName: "Sample",
syntaxTrees: new[] { tree },
references: new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
});
var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetCompilationUnitRoot();
var invocation = root
.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.First();
var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var method = (IMethodSymbol?)symbolInfo.Symbol;
Console.WriteLine(method?.ContainingType.ToDisplayString());
Console.WriteLine(method?.Name);
这样一来,就可以得到 Console.WriteLine 实际被解析成了哪个方法。
不仅有语法信息,还能利用编译器的名称解析结果,这正是 Roslyn 的优势所在。
9. 什么是 Symbol
在 Roslyn 中,类型、方法、属性、字段、参数、局部变量等都被当作 Symbol 来处理。
列举一些具有代表性的接口。
INamedTypeSymbol : 类、结构体、接口等
IMethodSymbol : 方法
IPropertySymbol : 属性
IFieldSymbol : 字段
IParameterSymbol : 参数
ILocalSymbol : 局部变量
INamespaceSymbol : 命名空间
Symbol 表示的不是源代码上的外观,而是编译器解析出来的语义。
比如,下面这两段代码外观不同。
System.Console.WriteLine("Hello");
using System;
Console.WriteLine("Hello");
但只要两者指向的都是同一个 System.Console.WriteLine,在 Roslyn 的语义分析中,就会被当作同一个方法符号来处理。
凭借这一特性,可以做出下面这些判断。
这个调用是否真的是本公司禁止使用的 API
这个类型是否实现了特定接口
这个方法是不是 async
这个返回值是不是 nullable
这个特性是否真的被标注了
这个类是否继承自特定的基类
这样就能基于编译器的判断来分析,而不只是单纯的字符串搜索。
10. 什么是 Compilation
Compilation 汇总了编译 C# 或 Visual Basic 程序所需的信息。
具体来说,它持有下面这些信息。
SyntaxTree 的集合
引用的汇编
编译选项
语言版本
预定义符号
类型与成员的信息
诊断信息
如果只是把单个文件当作语法来读取,SyntaxTree 就足够了。但要做类型解析或引用解析,就需要 Compilation。
比如想做下面这些事情时。
想知道这个方法调用属于哪个类型的方法
想知道这个类是否实现了 IDisposable
想知道这个特性实际对应哪个特性类型
想知道这个表达式的返回值类型
想获取编译错误或警告
这些仅凭语法是判断不出来的,需要连同项目的引用和编译选项一起考虑。
11. 什么是 Workspace
如果不是处理单个文件,而是想处理整个解决方案或项目,就要使用 Workspace。
Workspace 处理的是下面这些单位。
解决方案(Solution)
项目(Project)
文档(Document)
比如从整个解决方案中加载所有项目,并解析所有文档的场景。
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync("Sample.sln");
foreach (var project in solution.Projects)
{
Console.WriteLine(project.Name);
foreach (var document in project.Documents)
{
var root = await document.GetSyntaxRootAsync();
Console.WriteLine($" {document.Name}: {root?.DescendantNodes().Count()} nodes");
}
}
做出这样的工具,就可以用于现有代码库的调研和迁移辅助。
比如下面这些用途。
把特定 API 调用的清单导出为 CSV
列出旧命名空间的使用位置
生成 public API 的清单
调查项目之间的依赖关系
调查大型解决方案中的代码规范违规
进行机械式的代码转换
Analyzer 是与 IDE、构建过程一体运行的机制。而使用 Workspace 编写的命令行工具,则更适合用于调研和批量迁移。
这两者看起来相似,但最好分清各自的适用场景。
12. 使用 Roslyn 的几种典型方式
Roslyn 的用法大致可以分成 4 种。
1. 作为库来使用
2. 编写 Analyzer
3. 编写 Code Fix
4. 编写 Source Generator
它们各自的目的不同。
作为库来使用
从自制的命令行应用或内部工具中调用 Roslyn API。
适合的用途大致有这些。
代码库调研
批量转换
指标收集
迁移辅助
报告生成
这种形式下,可以在任意时机执行工具。因为不需要在 IDE 输入过程中运行,所以在一定程度上也更容易接受比较重的处理。
编写 Analyzer
Analyzer 是一种分析代码并给出警告或错误的机制。
比如可以制定下面这样的规则。
不使用 DateTime.Now,改用 DateTimeOffset.UtcNow
async 方法名必须以 Async 结尾
不能以错误的顺序调用库的初始化 API
新代码中禁止使用特定的 namespace
禁止使用 Task.Result / Wait
Analyzer 可以在 Visual Studio 中运行,也可以在构建时运行。这样团队的规范和库的使用方式,就不必依赖审查者的记忆,而可以机械化地被检测出来。
编写 Code Fix
Code Fix 是针对 Analyzer 发现的问题,给出修复方案的机制。
可以想象成 Visual Studio 里那个灯泡图标可以应用的修复,会比较容易理解。
比如假设 Analyzer 检测到了下面这段代码。
DateTime.Now
Code Fix 可以给出这样的修复建议。
DateTimeOffset.UtcNow
Code Fix 的优势在于,不仅仅是「发出警告」,还能把「安全的修复方式」自动化。
编写 Source Generator
Source Generator 是一种在编译期生成代码,并把该代码加入同一次编译的机制。
比如有下面这些用途。
从带有特性的类生成定型代码
从配置文件生成类型安全的访问器
生成 DTO 的映射代码
生成序列化器所需的代码
生成 enum 与字符串之间的转换代码
生成路由或 DI 注册代码
有些原本在运行时用 Reflection 收集信息的处理,可以替换成编译期生成的代码。这样做有时能降低启动成本,也有助于改善与 AOT 的兼容性。
13. 可以把 Analyzer 当作「自动代码审查」来用
在实务中,把 Analyzer 理解成「自动代码审查」会比较容易把握。
在代码审查中,有时会反复出现相同的指摘。
请不要使用这个 API
这个方法名不符合规范
这个 catch 把异常吞掉了
这个 null 检查是多余的
这个调用存在性能问题
如果人工每次都要指出这些问题,其中一部分就可以交给 Analyzer 来做。
尤其适合交给 Analyzer 的,是下面这类规则。
能够明确判断好坏
例外情况很少
修复方向已经确定
希望整个团队都遵守
在审查中频繁出现
可以接受因此中断构建
反过来,也有一些不适合交给 Analyzer 的情况。
高度依赖上下文、判断困难
需要做设计判断
例外情况太多
不同人意见分歧较大
警告太多导致没人再看
Analyzer 是一种强力工具。正因为它强力,一旦引入太多,反而会破坏开发体验。最好先从少数重要规则开始。
14. 从 .NET SDK 自带的 Analyzer 开始
在编写自定义 Analyzer 之前,先确认一下 .NET SDK 自带的 Analyzer,是更现实的做法。
在 .NET 5 及以后的项目中,.NET 的代码分析默认是启用的。
常见的诊断 ID 有下面这两个系列。
CAxxxx : 代码质量、可靠性、性能、安全性等
IDExxxx: 代码风格、IDE 支持等
Analyzer 的严重程度可以通过 .editorconfig 来调整。
# 示例:把未使用的 using 设为警告
dotnet_diagnostic.IDE0005.severity = warning
# 示例:把 CA2000 设为错误
dotnet_diagnostic.CA2000.severity = error
也可以在项目文件中进行启用或严格化设置。
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
在现有项目中引入时,最初可能会一次性冒出大量警告。这种情况下,不要一下子把全部都设成错误,而是分阶段推进比较好。
先掌握整体的警告数量
制定「新代码不再增加警告」的方针
只把重要规则设为 warning
只把真正必须遵守的规则设为 error
有计划地减少已有的违规
可以把自定义 Analyzer 理解成,在这个基础之上,补充「本公司特有的规则」。
15. Analyzer 的最小示例
Analyzer 会查找特定的语法或符号,并报告 Diagnostic。
比如来考虑一个对使用 DateTime.Now 发出警告的 Analyzer。
在实际的产品代码中,需要仔细处理类型解析和各种例外情况,但最小化的示例大致如下。
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoDateTimeNowAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
id: "CMP001",
title: "不要直接使用 DateTime.Now",
messageFormat: "请不要使用 DateTime.Now,根据用途考虑使用 DateTimeOffset.UtcNow 等替代方案",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(
AnalyzeMemberAccess,
SyntaxKind.SimpleMemberAccessExpression);
}
private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
if (memberAccess.Name.Identifier.Text != "Now")
{
return;
}
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is not IPropertySymbol propertySymbol)
{
return;
}
if (propertySymbol.Name == "Now" &&
propertySymbol.ContainingType.ToDisplayString() == "System.DateTime")
{
var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
这个例子中重要的一点是,它并不是单纯把 DateTime.Now 当作字符串来查找。
而是使用 SemanticModel 来确认它是否真的指向 System.DateTime.Now。
因此,就不容易把下面这种不相关的东西误判为目标。
MyCompany.DateTime.Now
在 Analyzer 中,经常会采用这样的流程:先用语法缩小候选范围,再通过语义分析确认是否真的是目标。
用 Syntax 快速查找候选项
用 SemanticModel 进行精确判定
用 Diagnostic 报告位置与消息
16. Code Fix 是连「修复方式」一起提供的机制
Analyzer 负责发现问题,Code Fix 则负责提示这个问题的修复方式。
比如检测到 DateTime.Now 时,可以提示这样的修复方案。
替换为 DateTimeOffset.UtcNow
替换为类似 IClock.Now 的抽象
不过,Code Fix 需要谨慎设计。并不是把 DateTime.Now 一律换成 DateTimeOffset.UtcNow 就万事大吉。想要显示本地时间的场景,和想要处理用于保存、比较的时间的场景,合适的类型和时区处理方式是不同的。
因此,Code Fix 更适合满足下面这些条件的场景。
修复后的含义明确
副作用很小
可以通过机械式转换安全地修复
人工容易确认
比如下面这类修复,就很适合用 Code Fix。
把旧的 API 名替换为新的 API 名
补充缺失的 using
按照命名规则修改名称
添加特性
删除不必要的参数
另一方面,对于需要做设计判断的修复,有时只发出警告而不做自动修复,反而更合适。
17. Source Generator 是「编译期的代码生成」
Source Generator 会在编译期运行,并把生成出来的 C# 代码添加到同一次编译中。
大致流程是这样的。
读取用户的源代码
检查特性和类型定义
生成所需的 C# 代码
把生成的代码加入编译对象
下面是一个简单的 Source Generator 示例。
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public sealed class BuildInfoGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static ctx =>
{
var source = """
namespace Generated;
public static class BuildInfo
{
public static string Tool => "Roslyn 源代码生成器";
}
""";
ctx.AddSource(
"BuildInfo.g.cs",
SourceText.From(source, Encoding.UTF8));
});
}
}
在引用了这个 Generator 的项目中,即使没有编写对应的源文件,也可以使用这个类型。
Console.WriteLine(Generated.BuildInfo.Tool);
Source Generator 不会改写用户已有的代码。它能做的,是生成额外的源代码并让其参与编译。
因此,这样理解会比较容易。
不是用来转换现有代码的
是查看现有代码、生成额外代码的
如果想对现有代码做批量改写,应该考虑使用基于 Roslyn 的迁移工具或 Code Fix,而不是 Source Generator。
18. Source Generator 的引用方式
开发过程中,如果要从另一个项目引用 Generator 项目,其处理方式与普通的库引用不同。
因为生成器不是运行时引用的库,而是在编译期作为 Analyzer 被加载的东西。
项目引用需要这样指定。
<ItemGroup>
<ProjectReference Include="..\BuildInfoGenerator\BuildInfoGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
通过设置 ReferenceOutputAssembly="false",可以避免把 Generator 的 DLL 当作普通的引用汇编来处理。
如果要以 NuGet 包的形式发布,也需要按照会被当作 Analyzer / Source Generator 加载的方式来配置。
Source Generator 很方便,但它的生命周期与普通库不同。
普通的库:在运行时被应用程序使用
Source Generator:在编译期被编译器使用
有必要意识到这个区别。
19. 适合用 Source Generator 的场景
Source Generator 并不是什么都拿来生成就好。
适合它的,是下面这类代码。
人工编写枯燥又容易出错
可以从输入信息机械地推导出来
生成结果易读
可以减少运行时 Reflection
有助于改善与 AOT、Trimming 的兼容性
可以提高类型安全性
举一些例子。
JSON 序列化用的元数据
DI 注册代码
配置值访问器
API 客户端
enum 转换代码
从 SQL 或 CSV 定义生成类型
INotifyPropertyChanged 的辅助代码
不过,如果生成出来的代码过于复杂,出问题时就很难追查。
使用 Source Generator 时,最好留意下面这些点。
让生成的代码可以被查看
让生成代码的名称保持稳定
让生成结果具有确定性
让出错时的 Diagnostic 易于理解
避免输入稍有变动就产生大量差异
生成出来的代码不应该看起来像魔法。重要的是要输出未来维护者也能读懂的代码。
20. 不适合用 Source Generator 的场景
也有一些处理不适合用 Source Generator。
依赖运行时状态的处理
需要网络访问的处理
依赖外部服务当前值的处理
每次结果都会变化的处理
改写现有源代码
对整个巨大解决方案做分析
因为 Generator 是在编译期运行的,速度慢的 Generator 会拖慢构建时间,也会破坏 IDE 的使用体验。
另外,依赖环境的 Generator 会引发下面这些问题。
在开发者的电脑上能通过,但在 CI 上失败
在 CI 上能通过,但在别的操作系统上失败
结果会随缓存状态变化
因网络故障导致构建失败
Source Generator 最好尽量向纯函数式处理靠拢。
输入:源代码、AdditionalFiles、AnalyzerConfigOptions
输出:生成的 C# 代码、Diagnostic
这个关系越明确,Generator 就越稳定。
21. 使用 Roslyn 编写代码调研工具
Roslyn 的用途不仅限于 Analyzer 和 Source Generator。在实务中,从自制的命令行工具中使用 Roslyn 同样很有效。
比如有下面这些需求。
想列出旧 API 的使用位置
想统计每个项目中 public class 的数量
想把带有特定特性的类导出为 CSV
想调查大型解决方案依赖的命名空间
想在 .NET Framework 迁移前梳理出依赖 Windows 的 API
在这类场景下,比起做成 Analyzer,做成单次执行或定期执行的调研工具,有时会更好处理。
举个例子,下面是列举解决方案内 public class 的简单示例。
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);
foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
var root = await document.GetSyntaxRootAsync();
if (root is null)
{
continue;
}
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Modifiers.Any(m => m.Text == "public"));
foreach (var cls in classes)
{
Console.WriteLine($"{project.Name},{document.FilePath},{cls.Identifier.Text}");
}
}
}
这个例子只看了语法。如果想调查「继承自特定基类的 public class」,就需要使用 SemanticModel 来查看类型的继承关系。
只需要名称或结构时用 Syntax
需要类型或引用目标时用 SemanticModel
需要处理整个项目时用 Workspace
这就是基本的使用区分方式。
22. 与正则表达式的区别
正则表达式很方便,但不适合用来处理 C# 代码的语义。
比如考虑下面这段代码。
// Console.WriteLine("debug");
用正则表达式查找 Console.WriteLine 时,可能会连注释里的字符串也一起找出来。
还有下面这种字符串字面量。
var text = "Console.WriteLine";
或者也可以跨行书写。
Console
.WriteLine("Hello");
此外,也可以使用别名。
using C = System.Console;
C.WriteLine("Hello");
用正则表达式很难正确处理这些情况。
使用 Roslyn,就可以把注释、字符串字面量、语法上的方法调用、以及实际解析出来的方法分开处理。
当然,如果只是简单的调查,grep 或 ripgrep 有时也足够。但如果要基于结果做设计判断或自动修复,使用 Roslyn 会更安全。
只是粗略查找就用字符串搜索
想按 C# 语义正确判定就用 Roslyn
23. 使用 Roslyn 调研现有资产
从 .NET Framework 迁移到现行 .NET 时,最先需要做的是「掌握现状」。这时候 Roslyn 就很有用。
比如下面这些调研。
依赖 System.Web 的清单
以 App.config / Web.config 为前提的代码清单
Windows Forms / WPF 特有 API 的使用位置
Remoting / BinaryFormatter 的使用位置
是否存在 COM 引用
P/Invoke 清单
尚未异步化的 I/O 处理
旧加密 API 的使用位置
单纯的字符串搜索也能列出候选项。但使用 Roslyn,就可以基于解析成类型或方法之后的结果来生成清单。
比如,光是查找 BinaryFormatter 这个字符串,也会把注释和文档里的内容一并拾取进来。
用 Roslyn 调查 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 类型的使用情况,就能得到更准确的候选清单。
在迁移工作中,不需要从一开始就做出完美的 Analyzer。哪怕只是做一个调研用的命令行工具,输出下面这样的 CSV,也已经很有价值。
Project,File,Line,Symbol,Kind
Legacy.Web,Controllers/HomeController.cs,42,System.Web.HttpContext.Current,Property
Legacy.Core,Serialization/OldStore.cs,18,System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,Type
有了这样的清单,就更容易制定迁移计划。
24. 对库开发者而言的 Roslyn
Roslyn 不仅对应用程序开发者有用,对库开发者同样有用。库都有其正确的使用方式。
比如下面这些规则。
必须先调用初始化方法
必须添加特定的特性
必须调用 Dispose
指定特定的选项是危险的
不希望新代码中使用已废弃的 API
如果只靠文档来传达这些规则,使用者有时会忽略掉。
如果把 Analyzer 一起打包进库的 NuGet 包中,就可以在使用者的代码上直接给出警告。
比如假设有一个自研库 Company.Messaging,就可以检测出下面这种误用。
var client = new MessageClient();
client.Send(message); // 在调用 Configure 之前就调用了 Send
Analyzer 可以给出这样的警告。
CMP1001: 请在调用 MessageClient.Send 之前先调用 Configure
再配合 Code Fix,还可以提示修复方案或示例代码,从而改善库的使用体验。
把写在文档里的内容,直接传达到使用者的编辑器上
这种思路,正是 Roslyn 的重要价值所在。
25. 通过 NuGet 分发 Analyzer 时的考量
Analyzer 可以以 NuGet 包的形式分发。不过需要把它与普通的运行时库分开来考虑,因为 Analyzer 并不是应用程序运行时所需要的东西,而是在构建时或 IDE 中被使用的东西。
因此,在设计包结构时要考虑下面这些点。
是否把运行时库与 Analyzer 放进同一个包
是否把 Analyzer 拆分成单独的包
是否默认发出警告
严重程度如何设定
是否可以通过 .editorconfig 控制
是否会突然给现有用户带来大量警告
如果只在公司内部使用,即便规则比较强硬,也可能更容易被接受。
如果作为公开库来发布,就需要注意不要突然破坏使用者的构建。
很多情况下,先从 Info 或 Warning 开始,让使用者可以按需自行提升到 Error,会更容易处理。
26. Roslyn 与 IDE 功能
在 Visual Studio 以及其他 .NET 开发环境中,Roslyn 的思路也深深影响着 IDE 功能。
比如下面这些功能。
IntelliSense
Go to Definition
Find All References
Rename
Extract Method
Quick Actions
代码风格警告
检测未使用的 using
这些功能都无法仅靠字符串搜索实现。比如在 Rename 中,绝不能误改到同名的其他符号。
class User
{
public string Name { get; set; }
}
class Product
{
public string Name { get; set; }
}
想修改 User.Name 时,不能连 Product.Name 也一起改掉。这就需要不仅依靠语法,还要按符号来区分。
Roslyn 的 API 为把这类 IDE 式的功能应用到自制工具中提供了基础。
27. 性能方面的注意事项
Roslyn 很强大,但如果写出很重的处理,自然会变慢。尤其是 Analyzer 和 Source Generator,有可能在开发者输入代码或构建过程中运行。
因此需要留意下面这些点。
避免不必要的 SemanticModel 获取
先用 Syntax 缩小候选范围,再做语义分析
避免文件 I/O
不进行网络访问
避免沉重的 Reflection
尊重取消请求
注意并行执行
不要把整个解决方案的分析放进 Analyzer 中
在 Analyzer 中,应尽可能缩小在 Initialize 中注册的对象范围。
下面是不好的例子。
先查看所有 SyntaxNode,再在内部用大量 if 语句判断
下面是更好的方向。
只注册需要的 SyntaxKind
先用名称或结构做轻量筛选
只在必要时才用 SemanticModel 做最终确认
Analyzer 有可能常驻在使用者的开发环境中。因此,除了准确性之外,轻量也是一种质量。
28. Diagnostic 的设计
Analyzer 给出的 Diagnostic,并不是随便发出警告就行。开发者看到之后,需要能够明白下面这些内容。
问题是什么
为什么是问题
应该修改哪里
应该怎么修改
是否存在例外情况
下面是不好的消息示例。
CMP001: 禁止使用
光看这个,完全不知道问题出在哪里。
下面是更好的示例方向。
CMP001: DateTime.Now 依赖于运行环境的本地时间。用于保存、比较的时间,请使用 DateTimeOffset.UtcNow 或时间提供器。
最好也提前设计好 Diagnostic ID 的编号方案。
CMP0001-CMP0999: 通用规则
CMP1000-CMP1999: 库 A 的规则
CMP2000-CMP2999: 迁移辅助规则
如果能准备文档页面,为 DiagnosticDescriptor 设置 HelpLinkUri 也是有效的做法。
警告本质上是与开发者之间的沟通。如果消息写得马虎,规则本身也会失去信任。
29. 严重程度的设计
Analyzer 的严重程度需要谨慎决定。具有代表性的等级如下。
Hidden / Silent
Info
Suggestion
Warning
Error
在实务中,很多时候不应该一开始就设为 Error。尤其当现有代码量很大时,一开始就设为 Error 会让引入工作停滞不前。
现实中,按下面这样的阶段来引入会比较容易。
1. 先以 Warning 的形式引入
2. 在 CI 中把警告数量可视化
3. 不再增加新的违规
4. 只把重要规则设为 Error
5. 制定减少现有违规的计划
Analyzer 的目的不是给开发者添麻烦,而是在不勉强的前提下,逐步提升代码库的质量。
30. Source Generator 的调试
Source Generator 的运行位置与普通应用程序不同,因此调试起来会有一些独特之处。
基本上可以用下面这些方法来调查。
查看生成的源代码
输出 Diagnostic
编写测试
必要时附加调试器
在 SDK 风格的项目中,使用输出生成文件的设置会更容易确认。
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
这样就能更容易确认生成出来的 .g.cs。
$(BaseIntermediateOutputPath) 通常指向 obj/ 目录下。
如果像 Generated 那样指定到项目根目录下,由于 SDK 风格项目默认会把 **/*.cs 纳入编译对象,下一次构建时,已经生成的 .g.cs 就会被当作普通源代码再次纳入,从而导致类型或成员重复的错误。
如果确实需要输出到项目根目录下,就要像 <Compile Remove="Generated/**/*.cs" /> 这样,明确地将其从编译对象中排除。
在 Generator 的测试中,经常采用对比输入代码与生成结果的方式。
准备输入源代码
运行 Generator
确认生成出来的源代码
确认预期的 Diagnostic
Source Generator 如果只靠人工确认动作,很快就会出问题。生成逻辑越复杂,测试就越重要。
31. 使用 Roslyn 编写测试
Analyzer 和 Source Generator 应该通过编写测试来逐步完善。尤其是 Analyzer,误报和漏报都会成为问题。
在测试中,需要准备下面这些模式。
应该被检测出来的代码
不应该被检测出来的代码
使用了 using alias 的代码
使用完全限定名的代码
使用名称相似但不同类型的代码
被当作 generated code 处理的代码
启用 nullable 时的代码
比如对于一个禁止使用 System.DateTime.Now 的 Analyzer,需要确认下面这些场景。
// 应该被检测出来
var x = System.DateTime.Now;
// 即使有 using,也应该被检测出来
using System;
var x = DateTime.Now;
// 如果是别的类型,就不应该被检测出来
namespace MyCompany;
public static class DateTime
{
public static string Now => "now";
}
var x = DateTime.Now;
最后这个例子,正是用字符串搜索容易出错的场景。在 Roslyn Analyzer 中,可以通过 SemanticModel 确认目标符号来避免这种错误。
32. 在 .NET Framework 项目中也能使用吗
Roslyn 并不是只适用于现行 .NET。不过,注意点会随着「怎么用」而变化。
作为调研工具使用时
把 Roslyn 工具做成 .NET 8 或 .NET 10 的命令行应用,去加载并分析 .NET Framework 的解决方案,是一个现实可行的选择。
这种情况下,工具本身运行在现行 .NET 上,而分析对象可以是 .NET Framework 的代码。
不过,用 MSBuildWorkspace 加载解决方案时,需要有能够构建目标项目的 MSBuild、SDK、引用汇编,以及 NuGet 还原环境。
也就是说,并不是只靠 Roslyn 就能读取一切,要真正解析出项目结构,还需要有相应的构建环境。
作为 Analyzer 使用时
Analyzer 是被编译器或 IDE 加载后运行的。
即便目标项目是 .NET Framework,只要编译器所在的环境能够加载 Analyzer,就可以使用。
但是,在旧版 csproj、旧版 Visual Studio、旧版 MSBuild,以及以 packages.config 为前提的配置下,引入和运维有时不像现行 SDK 风格那样顺畅。
在向现有 .NET Framework 项目引入时,最好先确认下面这些点。
Visual Studio / MSBuild 的版本
能否使用 PackageReference
CI 中能否运行相同的 Analyzer
警告是否会出现在构建日志中
.editorconfig 是否生效
作为 Source Generator 使用时
Source Generator 是在编译期由编译器加载的机制。
因此,比起目标项目的运行时框架,构建所用的编译器和 SDK 的支持情况更为重要。
在现行 .NET 的 SDK 风格项目中比较容易处理,而在旧版 .NET Framework 项目中,则需要根据项目格式和构建环境留意各种细节。
对于 .NET Framework 的现有资产,比起从一开始就引入 Source Generator,先从基于 Roslyn 的调研工具或 Analyzer 开始,通常会更安全。
33. 版本选择的注意事项
与 Roslyn 相关的 NuGet 包中,包含了 Microsoft.CodeAnalysis.* 系列。
具有代表性的有下面这些。
Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
Microsoft.CodeAnalysis.Workspaces.MSBuild
Microsoft.CodeAnalysis.Analyzers
Microsoft.CodeAnalysis.CSharp.CodeFix.Testing
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing
这里需要注意的是,Analyzer 和 Source Generator 是被使用者一侧的编译器加载的。
也就是说,如果开发者本地或 CI 上的 SDK / Visual Studio 版本较旧,使用了过新 Roslyn API 的 Analyzer / Generator 就可能无法运行。
如果只在公司内部使用,并且能统一构建环境,就比较容易使用相对较新的 API。
而对于对外发布的库,则需要考虑到广泛的使用者环境,谨慎选择所依赖的 Microsoft.CodeAnalysis 版本。
作为方针,可以这样考虑。
公司内部专用:在统一 CI 与开发环境的前提下,使用较新的 API
对外发布:考虑使用者的 SDK / VS 范围,保守地选择版本
Generator:尽可能设计成 Incremental Generator
Analyzer:优先保证不破坏 IDE 体验的轻量性
Roslyn 属于接近编译器的领域,因此容易受到版本差异的影响。
34. 不要试图用 Roslyn 解决一切
Roslyn 很强大,但它不是能解决一切问题的工具。比如,下面这些问题光靠 Roslyn 是解决不了的。
运行时会走哪条分支
生产环境数据会传入什么值
通过 Reflection 动态调用的方法
DI 容器在运行时的注册结果
随配置文件变化的处理
外部服务返回的值
Roslyn 主要是处理源代码与编译信息的工具。如果想了解运行时的行为,就需要测试、日志、追踪、性能分析、dump 分析等其他手段。
因此,可以这样理解 Roslyn 的角色。
以高精度处理静态可知的信息
如果硬要用 Roslyn 去解决那些只能在动态中才能知道的事情,就会做出既复杂又不准确的机制。
35. 引入的顺序
如果要在实务中开始使用 Roslyn,推荐按照下面这个顺序。
1. 先整理好现有的 .NET Analyzer 和 .editorconfig
2. 编写使用 Syntax Tree 的小型调研工具
3. 尝试用 SemanticModel 做类型解析
4. 用 MSBuildWorkspace 读取解决方案
5. 编写团队专属的小型 Analyzer
6. 如有需要,添加 Code Fix
7. 在定型代码较多的地方考虑引入 Source Generator
不需要一开始就直奔 Source Generator。在很多现场,Analyzer 和调研工具往往更容易先见到效果。
尤其是当现有资产规模较大时,下面这样的流程会更现实。
用调研工具掌握现状
把频繁出现的问题做成 Analyzer
只把能安全修复的部分做成 Code Fix
把反复编写的定型代码做成 Generator
Roslyn 是一种可以分阶段使用的工具。
36. 小示例:列出方法调用清单
最后,我们再用一个更贴近实务的形式,来看看 Roslyn 的用法。这里展示的是列出解决方案内方法调用清单的示例。
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);
foreach (var project in solution.Projects)
{
var compilation = await project.GetCompilationAsync();
if (compilation is null)
{
continue;
}
foreach (var document in project.Documents)
{
var tree = await document.GetSyntaxTreeAsync();
if (tree is null)
{
continue;
}
var root = await tree.GetRootAsync();
var semanticModel = compilation.GetSemanticModel(tree);
var invocations = root
.DescendantNodes()
.OfType<InvocationExpressionSyntax>();
foreach (var invocation in invocations)
{
var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
if (symbol is null)
{
continue;
}
var lineSpan = invocation.GetLocation().GetLineSpan();
var line = lineSpan.StartLinePosition.Line + 1;
Console.WriteLine(string.Join(",", new[]
{
project.Name,
document.FilePath ?? document.Name,
line.ToString(),
symbol.ContainingType.ToDisplayString(),
symbol.Name
}));
}
}
}
这样的工具再稍加扩展,就能做到下面这些调研。
只提取特定方法的调用
输出已废弃 API 的使用位置
按项目输出使用频率
生成迁移目标 API 的清单
如果能站在编译器的视角阅读源代码,对现有代码的调研会轻松很多。
37. 用 Roslyn 改写代码时的注意事项
在 Roslyn 中,也可以利用语法树来改写代码。
比如可以修改特定的方法名、添加特性,或者添加 using。
不过,改写代码应该谨慎进行。需要注意的点大致如下。
确认改写后含义不变
不破坏注释和空白
避免差异过大
统一格式
不要一次性进行太多转换
让 Git 差异易于审查
由于 Roslyn 的 Syntax Tree 会保留 Trivia,因此可以在保留注释和空白的前提下进行转换。但如果粗率地创建节点,生成出来的代码格式有时会被破坏。
编写改写工具时,按下面这个方针会比较安全。
先只做检测
确认转换前后的差异
从小规模转换开始
为转换工具本身编写测试
在 CI 中先从检测模式开始
在大规模机械转换中,Roslyn 很强大,但最后仍然需要人工审查。
38. Roslyn 与 AI 编程辅助
近年来,借助 AI 进行代码生成和审查辅助已经变得很普遍,但即便如此,Roslyn 的价值也并未降低。AI 擅长处理自然语言和周边上下文,而 Roslyn 作为编译器,擅长处理精确的语法与语义信息。两者与其说是竞争关系,更像是互补关系。
比如可以考虑下面这样的分工。
用 Roslyn 精确提取目标位置
用 AI 生成修复方案或说明文字
用 Roslyn 确认修复方案是否能够编译通过
用 Analyzer 防止问题再次发生
有时候,比起直接拜托 AI「把这个代码库里所有旧 API 都修好」,先用 Roslyn 精确提取出目标位置会更安全。
然后再把 AI 用在修复方案的探讨和审查辅助上,会更贴近实务。
编译器能知道的事情就交给编译器。人和 AI 则专注于其之上的判断。
这种分工非常重要。
39. 实务检查清单
在使用 Roslyn 之前,最好先确认下面这些内容。
目的是调研、警告、修复,还是生成
只用语法就够了,还是需要语义分析
只处理单个文件就行,还是需要整个项目
是否需要在 IDE 上运行,还是单次工具就够了
是否可以接受影响构建时间
是否要在 CI 中运行
现有代码是否会因此产生大量警告
Analyzer 的严重程度如何设定
Code Fix 是否能安全地应用
Source Generator 生成的代码是否可以查看
使用者的 SDK / Visual Studio 版本是否统一
如果拿不定主意,可以按下面这样区分。
想调研 -> 基于 Roslyn 的命令行工具
想始终强制遵守 -> Analyzer
修复方式已经确定 -> Code Fix
想生成定型代码 -> Source Generator
按这种方式区分,就不容易用错 Roslyn 的场景。
40. 总结
Roslyn 把 C# 与 Visual Basic 的编译器,以开发者可以使用的 API 的形式开放出来。
使用 Roslyn,就可以不把源代码当作单纯的字符串,而是以下面这些形式来处理。
以 Syntax Tree 的形式读取语法
以 SemanticModel 的形式读取语义
以 Compilation 的形式处理整体编译
以 Workspace 的形式处理解决方案或项目
以 Analyzer 的形式发出警告
以 Code Fix 的形式提示修复方案
以 Source Generator 的形式生成代码
在实务中,尤其在下面这些场景中很有帮助。
现有代码库的调研
从 .NET Framework 向 .NET 的迁移支持
团队规范的自动检查
对库使用者的引导
定型代码的生成
在 IDE 或 CI 中保障质量
重要的是,不要把 Roslyn 太当作一门「高深的编译器技术」来构筑心理门槛。
最开始,只要用 CSharpSyntaxTree.ParseText 读取一个文件、列举出方法名就足够了。之后再逐步扩展到 SemanticModel、Workspace、Analyzer、Source Generator 就可以了。
如果要用一句话来概括 Roslyn,大概是这样。
让 C# 代码不再是字符串,而是可以作为编译器所理解的结构来处理。
带着这个视角,代码审查、迁移、调研、生成的自动化都会更容易推进一个层次。
参考资料
- 本文的完整示例代码(库、Analyzer / Source Generator、演示、单元测试) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/roslyn-dotnet-compiler-platform
- dotnet/roslyn - GitHub
- The .NET Compiler Platform SDK - Microsoft Learn
- Work with syntax - Microsoft Learn
- Work with semantics - Microsoft Learn
- Work with a workspace - Microsoft Learn
- Overview of .NET source code analysis - Microsoft Learn
- Code analysis using .NET compiler platform analyzers - Microsoft Learn
- Get started with Roslyn analyzers - Microsoft Learn
- Introducing C# Source Generators - .NET Blog
- Source Generator Cookbook - GitHub
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
正确处理 Windows 的模拟令牌 ── 线程级权限借用与安全的还原方式
本文围绕 Windows 的模拟令牌,梳理访问令牌、主令牌、线程令牌、模拟级别、RevertToSelf,直至 .NET 的 WindowsIdentity.RunImpersonated,整理在实务中安全处理它们的思路。
把OSI参考模型彻底看懂 ── 拆解一个HTTP请求里的七层结构
本文不靠死记硬背来理解OSI参考模型,而是从实物出发。用C#组装出一个承载HTTP GET请求的Ethernet帧并进行解剖,通过十六进制转储和Wireshark确认L2〜L7如何以字节序列的形式层层物理嵌套在一起。文章从实务角度梳理各层与.NET API(HttpClie...
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
在 C# 中安全调用 Win32 API —— P/Invoke 实务指南(DllImport / LibraryImport / CsWin32)
本文整理在 C# 中通过 P/Invoke 调用 Win32 API 或原生 DLL 时的实务要点:DllImport 与 LibraryImport 的区别、用 CsWin32 自动生成签名、字符串封送的陷阱、用 SafeHandle 管理句柄、SetLastError ...
常见问题
汇总了咨询这一主题时常见的问题。
- Roslyn 是什么?
- Roslyn 正式名称为 .NET Compiler Platform,是 C# 与 Visual Basic 的编译器实现,同时也是一组用于构建代码分析工具的 API。以往编译器内部生成的信息(这个标识符属于哪个类型、这个调用指向哪个方法等)一直是一个黑盒,Roslyn 把这些信息开放给应用程序和工具使用。这样一来,就可以把 C# 代码不当作字符串而是当作语法来读取,不看外观而看含义来理解,并据此给出警告、修复建议和生成代码。
- 使用 Roslyn 能做什么?
- 可以对 C# / VB 做语法分析、对类型和方法做语义分析、对整个项目或解决方案做分析,还可以编写自定义的 Analyzer、Code Fix、Source Generator,以及进行代码生成与代码转换。更贴近实务的说法是:把使用了禁用 API 的代码标记为构建警告、列出使用旧 API 的位置、在编译期生成 DTO 映射代码、辅助从 .NET Framework 迁移到 .NET 的调研等。用法大体可以分成四类:作为库使用、编写 Analyzer、编写 Code Fix、编写 Source Generator。
- 与正则表达式或 grep 做代码搜索相比,Roslyn 有什么不同?
- 正则表达式很难正确处理注释中的字符串、字符串字面量、跨行书写的调用,以及通过 using alias 使用的别名调用。使用 Roslyn,就可以把注释、字符串字面量、语法层面的方法调用,以及实际解析出来的方法分开处理。如果只是粗略地查找,字符串搜索通常也够用;但如果要基于结果做设计判断或自动修复,依赖编译器名称解析结果的 Roslyn 会更安全。
- 应该从哪里开始学习 Roslyn?
- 不需要一开始就直奔 Source Generator。建议先整理好现有 .NET SDK 自带的 Analyzer 和 .editorconfig,然后用 CSharpSyntaxTree.ParseText 读取一个文件、写一个列举方法名的小调研工具,再逐步扩展到用 SemanticModel 做类型解析、用 MSBuildWorkspace 读取整个解决方案,最后再编写团队专属的小型 Analyzer。在很多现场,Analyzer 与调研工具比 Source Generator 更容易先见到效果。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。