语法树基础操作
简介
本文档详细介绍 Roslyn 语法树的基础概念和操作。你将学习什么是语法树、三种节点类型、如何创建和解析语法树,以及如何进行基本的查询和过滤操作。
适合人群: 初学者和中级开发者
预计阅读时间: 65 分钟
前置知识: C# 基础语法
📋 本文导航
| 章节 | 难度 | 阅读时间 | 链接 |
|---|---|---|---|
| 什么是语法树? | 🟢 基础 | 5 分钟 | 查看 |
| 语法树的特点 | 🟢 基础 | 10 分钟 | 查看 |
| 三种节点类型 | 🟢 基础 | 15 分钟 | 查看 |
| 语法树示例 | 🟢 基础 | 10 分钟 | 查看 |
| 创建和解析语法树 | 🟡 中级 | 15 分钟 | 查看 |
| 语法树的属性和方法 | 🟡 中级 | 10 分钟 | 查看 |
| 查询和过滤节点 | 🟢 基础 | 10 分钟 | 查看 |
🟢 什么是语法树?
语法树(Syntax Tree)是源代码的结构化表示形式。它将源代码文本转换为树形的数据结构,使得程序能够理解和操作代码的结构。
为什么需要语法树?
在传统的文本处理中,代码只是一串字符。但对于编译器、代码分析工具和 IDE 来说,需要理解代码的结构:
- 哪些是类声明?
- 哪些是方法?
- 表达式的计算顺序是什么?
- 变量的作用域在哪里?
语法树提供了这种结构化的视图,让我们能够:
- 分析代码: 查找特定的代码模式
- 生成代码: 创建新的代码结构
- 重构代码: 修改现有代码的结构
- 理解代码: 获取代码的语义信息
简单示例
让我们看一个简单的例子:
public class Person
{
public string Name { get; set; }
}这段代码在语法树中的表示:
CompilationUnit (根节点)
└── ClassDeclaration (类声明)
├── Modifiers: [public]
├── Keyword: class
├── Identifier: Person
└── Members (成员列表)
└── PropertyDeclaration (属性声明)
├── Modifiers: [public]
├── Type: string
├── Identifier: Name
└── AccessorList (访问器列表)
├── GetAccessor (get 访问器)
└── SetAccessor (set 访问器)提示
语法树不仅包含代码结构,还包含所有的空格、换行、注释等信息,这使得它能够完整地重建原始源代码。
🟢 语法树的特点
Roslyn 的语法树具有三个重要特点,理解这些特点对于有效使用语法树至关重要。
1. 完整性(Fidelity)
语法树包含源代码的所有信息,包括:
- 代码本身: 所有的声明、语句、表达式
- 空格和换行: 保留原始格式
- 注释: 单行注释、多行注释、文档注释
- 预处理指令:
#if、#define等
这意味着你可以从语法树完全重建原始源代码,包括所有的格式和注释。
var code = @"
// 这是一个注释
public class Person // 类定义
{
public string Name { get; set; }
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 可以完全重建原始代码
Console.WriteLine(root.ToFullString()); // 包含所有空格和注释2. 不可变性(Immutability)
语法树一旦创建就不能修改。如果需要修改,必须创建一个新的树。
为什么要不可变?
- 线程安全: 多个线程可以安全地读取同一个语法树
- 可预测性: 不用担心树在使用过程中被意外修改
- 性能优化: 可以安全地共享和缓存节点
var tree = CSharpSyntaxTree.ParseText("class Person { }");
var root = tree.GetRoot();
// 不能直接修改,只能创建新的树
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 创建一个新的类声明,修改名称
var newClassDecl = classDecl.WithIdentifier(
SyntaxFactory.Identifier("Employee"));
// 创建一个新的根节点
var newRoot = root.ReplaceNode(classDecl, newClassDecl);注意
修改操作总是返回新的节点或树,原始的节点和树保持不变。
3. 红绿树结构(Red-Green Tree)
Roslyn 使用一种称为"红绿树"的内部结构来优化内存使用和性能。
- 绿树(Green Tree): 不可变的、共享的节点,不包含位置信息
- 红树(Red Tree): 包含位置信息的节点,是我们通常操作的节点
这种设计允许:
- 内存共享: 相同的代码结构可以共享绿节点
- 高效修改: 只需要创建改变部分的新节点
- 延迟计算: 位置信息按需计算
提示
作为用户,你通常不需要关心红绿树的细节。Roslyn 会自动处理这些优化。如果你想深入了解性能优化,请参阅 性能优化文档。
🟢 三种节点类型
Roslyn 语法树由三种类型的节点组成,每种节点都有特定的用途。
1. SyntaxNode(语法节点)
SyntaxNode 表示代码的结构化元素,如声明、语句、表达式等。这是语法树中最重要的节点类型。
常见的 SyntaxNode 类型
| 类型 | 说明 | 示例 |
|---|---|---|
ClassDeclarationSyntax | 类声明 | public class Person { } |
MethodDeclarationSyntax | 方法声明 | public void DoWork() { } |
PropertyDeclarationSyntax | 属性声明 | public string Name { get; set; } |
IfStatementSyntax | if 语句 | if (x > 0) { } |
ForStatementSyntax | for 循环 | for (int i = 0; i < 10; i++) { } |
InvocationExpressionSyntax | 方法调用 | Console.WriteLine("Hello") |
BinaryExpressionSyntax | 二元表达式 | a + b |
使用示例
var code = @"
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 获取类声明
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
Console.WriteLine($"类名: {classDecl.Identifier.Text}");
// 获取方法声明
var methodDecl = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.First();
Console.WriteLine($"方法名: {methodDecl.Identifier.Text}");
Console.WriteLine($"返回类型: {methodDecl.ReturnType}");2. SyntaxToken(语法标记)
SyntaxToken 表示代码的最小语法单元,如关键字、标识符、运算符、字面量等。Token 是语法树的叶子节点。
常见的 SyntaxToken 类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 关键字 | C# 保留字 | public, class, if, return |
| 标识符 | 变量名、类名等 | Person, name, Calculate |
| 运算符 | 操作符 | +, -, *, ==, && |
| 字面量 | 常量值 | 123, "Hello", true |
| 符号 | 分隔符和括号 | {, }, (, ), ; |
使用示例
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 获取 class 关键字 token
var classKeyword = classDecl.Keyword;
Console.WriteLine($"关键字: {classKeyword.Text}");
Console.WriteLine($"类型: {classKeyword.Kind()}");
// 获取类名 token
var identifier = classDecl.Identifier;
Console.WriteLine($"标识符: {identifier.Text}");
Console.WriteLine($"位置: {identifier.SpanStart}");3. SyntaxTrivia(琐碎内容)
SyntaxTrivia 表示对代码结构不重要但对格式和文档很重要的内容,如空格、换行、注释等。
常见的 SyntaxTrivia 类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 空格 | 空格字符 | |
| 换行 | 换行符 | \n, \r\n |
| 单行注释 | // 注释 | // 这是注释 |
| 多行注释 | /* */ 注释 | /* 多行注释 */ |
| 文档注释 | /// 注释 | /// <summary>...</summary> |
| 预处理指令 | 编译指令 | #if DEBUG |
使用示例
var code = @"
// 这是一个类
public class Person
{
/*
* 这是一个属性
*/
public string Name { get; set; }
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 获取所有 token
foreach (var token in root.DescendantTokens())
{
// 获取 token 前面的 trivia(注释、空格等)
foreach (var trivia in token.LeadingTrivia)
{
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
{
Console.WriteLine($"单行注释: {trivia}");
}
else if (trivia.IsKind(SyntaxKind.MultiLineCommentTrivia))
{
Console.WriteLine($"多行注释: {trivia}");
}
}
}提示
Trivia 附加在 Token 上,每个 Token 可以有前导 trivia(LeadingTrivia)和尾随 trivia(TrailingTrivia)。
🟢 语法树示例
让我们通过一个完整的示例来理解语法树的结构。
源代码
using System;
namespace MyApp
{
/// <summary>
/// 表示一个人
/// </summary>
public class Person
{
// 姓名属性
public string Name { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello, {Name}!");
}
}
}语法树结构
点击查看完整的树结构
CompilationUnit
├── Usings
│ └── UsingDirective
│ ├── UsingKeyword: "using"
│ ├── Name: "System"
│ └── SemicolonToken: ";"
├── Members
│ └── NamespaceDeclaration
│ ├── NamespaceKeyword: "namespace"
│ ├── Name: "MyApp"
│ ├── OpenBraceToken: "{"
│ ├── Members
│ │ └── ClassDeclaration
│ │ ├── LeadingTrivia: [DocumentationComment]
│ │ ├── Modifiers: [public]
│ │ ├── Keyword: "class"
│ │ ├── Identifier: "Person"
│ │ ├── OpenBraceToken: "{"
│ │ ├── Members
│ │ │ ├── PropertyDeclaration
│ │ │ │ ├── LeadingTrivia: [SingleLineComment]
│ │ │ │ ├── Modifiers: [public]
│ │ │ │ ├── Type: "string"
│ │ │ │ ├── Identifier: "Name"
│ │ │ │ └── AccessorList
│ │ │ │ ├── GetAccessor
│ │ │ │ └── SetAccessor
│ │ │ └── MethodDeclaration
│ │ │ ├── Modifiers: [public]
│ │ │ ├── ReturnType: "void"
│ │ │ ├── Identifier: "SayHello"
│ │ │ ├── ParameterList: "()"
│ │ │ └── Body
│ │ │ └── ExpressionStatement
│ │ │ └── InvocationExpression
│ │ │ ├── Expression: "Console.WriteLine"
│ │ │ └── ArgumentList
│ │ │ └── Argument
│ │ │ └── InterpolatedStringExpression
│ │ └── CloseBraceToken: "}"
│ └── CloseBraceToken: "}"
└── EndOfFileToken遍历示例
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 获取命名空间
var namespaceDecl = root.DescendantNodes()
.OfType<NamespaceDeclarationSyntax>()
.First();
Console.WriteLine($"命名空间: {namespaceDecl.Name}");
// 获取类声明
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
Console.WriteLine($"类名: {classDecl.Identifier.Text}");
// 获取文档注释
var docComment = classDecl.GetLeadingTrivia()
.FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia));
if (docComment != default)
{
Console.WriteLine($"文档注释: {docComment}");
}
// 获取所有方法
var methods = classDecl.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
Console.WriteLine($"方法: {method.Identifier.Text}");
}🟡 创建和解析语法树
Roslyn 提供了多种方式来创建和解析语法树。
从文本创建
最简单的方式是从字符串创建语法树:
using Microsoft.CodeAnalysis.CSharp;
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();从文件解析
从文件读取并解析:
using System.IO;
using Microsoft.CodeAnalysis.CSharp;
var filePath = "Person.cs";
var code = File.ReadAllText(filePath);
var tree = CSharpSyntaxTree.ParseText(code, path: filePath);提示
指定 path 参数可以让错误消息包含文件名,便于调试。
使用解析选项
可以指定解析选项来控制解析行为:
var options = new CSharpParseOptions(
languageVersion: LanguageVersion.CSharp10,
kind: SourceCodeKind.Regular,
documentationMode: DocumentationMode.Parse
);
var tree = CSharpSyntaxTree.ParseText(code, options);常用选项:
| 选项 | 说明 | 值 |
|---|---|---|
languageVersion | C# 语言版本 | CSharp7, CSharp8, CSharp9, CSharp10, CSharp11 |
kind | 代码类型 | Regular(普通代码), Script(脚本代码) |
documentationMode | 文档注释模式 | Parse(解析), Diagnose(诊断), None(忽略) |
增量解析
Roslyn 支持增量解析,可以高效地处理代码修改:
var originalCode = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(originalCode);
// 修改代码
var newCode = "public class Employee { }";
var newTree = tree.WithChangedText(
SourceText.From(newCode));
// Roslyn 会尽可能重用原有的节点性能提示
增量解析比完全重新解析快得多,特别是对于大型文件。IDE 使用这种技术来提供实时的代码分析。
处理语法错误
Roslyn 可以解析包含语法错误的代码:
var code = "public class Person {"; // 缺少右花括号
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 检查是否有错误
var diagnostics = tree.GetDiagnostics();
foreach (var diagnostic in diagnostics)
{
Console.WriteLine($"错误: {diagnostic.GetMessage()}");
Console.WriteLine($"位置: {diagnostic.Location.GetLineSpan()}");
}
// 即使有错误,仍然可以获取语法树
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault();
if (classDecl != null)
{
Console.WriteLine($"找到类: {classDecl.Identifier.Text}");
}注意
即使代码有语法错误,Roslyn 也会尽力构建语法树。这使得 IDE 能够在你输入代码时提供智能提示。
🟡 语法树的属性和方法
SyntaxNode 提供了丰富的属性和方法来导航和查询语法树。
常用属性
| 属性 | 说明 | 示例 |
|---|---|---|
Parent | 父节点 | node.Parent |
ChildNodes() | 直接子节点 | node.ChildNodes() |
DescendantNodes() | 所有后代节点 | node.DescendantNodes() |
Ancestors() | 所有祖先节点 | node.Ancestors() |
Span | 节点的文本范围(不含 trivia) | node.Span |
FullSpan | 节点的完整范围(含 trivia) | node.FullSpan |
SyntaxTree | 所属的语法树 | node.SyntaxTree |
导航方法
var code = @"
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 获取直接子节点
var children = classDecl.ChildNodes();
Console.WriteLine($"子节点数: {children.Count()}");
// 获取所有后代节点
var descendants = classDecl.DescendantNodes();
Console.WriteLine($"后代节点数: {descendants.Count()}");
// 获取特定类型的后代
var properties = classDecl.DescendantNodes()
.OfType<PropertyDeclarationSyntax>();
Console.WriteLine($"属性数: {properties.Count()}");
// 获取父节点
var parent = classDecl.Parent;
Console.WriteLine($"父节点类型: {parent.GetType().Name}");
// 获取祖先节点
var ancestors = classDecl.Ancestors();
foreach (var ancestor in ancestors)
{
Console.WriteLine($"祖先: {ancestor.GetType().Name}");
}位置信息
var propertyDecl = root.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.First();
// 获取文本范围(不含前导和尾随 trivia)
var span = propertyDecl.Span;
Console.WriteLine($"起始位置: {span.Start}");
Console.WriteLine($"结束位置: {span.End}");
Console.WriteLine($"长度: {span.Length}");
// 获取完整范围(含 trivia)
var fullSpan = propertyDecl.FullSpan;
Console.WriteLine($"完整长度: {fullSpan.Length}");
// 获取行列信息
var lineSpan = tree.GetLineSpan(propertyDecl.Span);
Console.WriteLine($"起始行: {lineSpan.StartLinePosition.Line}");
Console.WriteLine($"起始列: {lineSpan.StartLinePosition.Character}");文本内容
// 获取节点的文本(不含 trivia)
var text = propertyDecl.ToString();
Console.WriteLine($"文本: {text}");
// 获取完整文本(含 trivia)
var fullText = propertyDecl.ToFullString();
Console.WriteLine($"完整文本: {fullText}");
// 获取源文本
var sourceText = tree.GetText();
var nodeText = sourceText.GetSubText(propertyDecl.Span);
Console.WriteLine($"源文本: {nodeText}");🟢 查询和过滤节点
掌握查询和过滤技巧是高效使用语法树的关键。
基本查询
查找特定类型的节点
var code = @"
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($""Hello, {Name}!"");
}
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 查找所有类
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>();
// 查找所有属性
var properties = root.DescendantNodes()
.OfType<PropertyDeclarationSyntax>();
// 查找所有方法
var methods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
// 查找所有方法调用
var invocations = root.DescendantNodes()
.OfType<InvocationExpressionSyntax>();查找第一个匹配的节点
// 查找第一个类
var firstClass = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault();
// 查找第一个公共方法
var firstPublicMethod = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(m => m.Modifiers.Any(
mod => mod.IsKind(SyntaxKind.PublicKeyword)));条件过滤
按修饰符过滤
// 查找所有公共类
var publicClasses = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Modifiers.Any(
m => m.IsKind(SyntaxKind.PublicKeyword)));
// 查找所有静态方法
var staticMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.Modifiers.Any(
mod => mod.IsKind(SyntaxKind.StaticKeyword)));
// 查找所有抽象类
var abstractClasses = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Modifiers.Any(
m => m.IsKind(SyntaxKind.AbstractKeyword)));按名称过滤
// 查找名为 "Person" 的类
var personClass = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault(c => c.Identifier.Text == "Person");
// 查找所有以 "Get" 开头的方法
var getMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.Identifier.Text.StartsWith("Get"));
// 查找所有名称包含 "Name" 的属性
var nameProperties = root.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Where(p => p.Identifier.Text.Contains("Name"));按类型过滤
// 查找所有返回 string 的方法
var stringMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.ReturnType.ToString() == "string");
// 查找所有 int 类型的属性
var intProperties = root.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Where(p => p.Type.ToString() == "int");使用 LINQ 进行复杂查询
组合多个条件
// 查找所有公共的、非静态的、返回 void 的方法
var methods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m =>
m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword)) &&
!m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.StaticKeyword)) &&
m.ReturnType.ToString() == "void");分组和统计
// 按返回类型分组方法
var methodsByReturnType = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.GroupBy(m => m.ReturnType.ToString())
.Select(g => new
{
ReturnType = g.Key,
Count = g.Count(),
Methods = g.Select(m => m.Identifier.Text).ToList()
});
foreach (var group in methodsByReturnType)
{
Console.WriteLine($"返回类型 {group.ReturnType}: {group.Count} 个方法");
foreach (var methodName in group.Methods)
{
Console.WriteLine($" - {methodName}");
}
}查找嵌套结构
// 查找所有嵌套类
var nestedClasses = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Parent is ClassDeclarationSyntax);
// 查找所有在 if 语句中的方法调用
var invocationsInIf = root.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Where(inv => inv.Ancestors()
.OfType<IfStatementSyntax>()
.Any());高级查询技巧
使用谓词函数
// 定义一个谓词函数
bool IsPublicProperty(PropertyDeclarationSyntax property)
{
return property.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)) &&
property.AccessorList?.Accessors.Count == 2; // 有 get 和 set
}
// 使用谓词
var publicProperties = root.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Where(IsPublicProperty);递归查询
// 查找所有包含特定方法调用的方法
var methodsCallingConsole = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Any(inv => inv.Expression.ToString().StartsWith("Console")));性能优化技巧
性能提示
- 一次遍历收集多种信息: 避免多次调用
DescendantNodes() - 使用
FirstOrDefault()而不是First(): 避免在找不到时抛出异常 - 缓存查询结果: 如果需要多次使用,先转换为列表
- 使用
OfType<T>()而不是Where(n => n is T): 性能更好
// ❌ 不好:多次遍历
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
var properties = root.DescendantNodes().OfType<PropertyDeclarationSyntax>();
// ✅ 好:一次遍历
var allNodes = root.DescendantNodes().ToList();
var classes = allNodes.OfType<ClassDeclarationSyntax>();
var methods = allNodes.OfType<MethodDeclarationSyntax>();
var properties = allNodes.OfType<PropertyDeclarationSyntax>();🔗 相关 API
核心 API 文档
- SyntaxNode API 参考 - 语法节点的基类
- SyntaxToken API 参考 - 语法标记结构
- SyntaxTrivia API 参考 - 琐碎内容结构
- SyntaxTree API 参考 - 语法树基类
- CSharpSyntaxTree API 参考 - C# 语法树
C# 特定语法节点
- ClassDeclarationSyntax - 类声明
- MethodDeclarationSyntax - 方法声明
- PropertyDeclarationSyntax - 属性声明
- IfStatementSyntax - if 语句
- InvocationExpressionSyntax - 方法调用表达式
工具和资源
- Roslyn Quoter - 可视化语法树结构
- Roslyn SDK - 官方 SDK 和示例
- Syntax Visualizer - Visual Studio 语法可视化工具
📚 下一步
现在你已经掌握了语法树的基础知识,可以继续学习:
或者返回 语法树概览 查看快速参考和速查表。
实践建议
- 使用 Roslyn Quoter 来可视化你的代码的语法树结构
- 在 Visual Studio 中安装 Syntax Visualizer 扩展来实时查看语法树
- 尝试编写一些简单的代码分析工具来练习查询和过滤技巧