Skip to content

语法树基础操作

简介

本文档详细介绍 Roslyn 语法树的基础概念和操作。你将学习什么是语法树、三种节点类型、如何创建和解析语法树,以及如何进行基本的查询和过滤操作。

适合人群: 初学者和中级开发者
预计阅读时间: 65 分钟
前置知识: C# 基础语法


📋 本文导航

章节难度阅读时间链接
什么是语法树?🟢 基础5 分钟查看
语法树的特点🟢 基础10 分钟查看
三种节点类型🟢 基础15 分钟查看
语法树示例🟢 基础10 分钟查看
创建和解析语法树🟡 中级15 分钟查看
语法树的属性和方法🟡 中级10 分钟查看
查询和过滤节点🟢 基础10 分钟查看

返回: 语法树概览 | 下一篇: 遍历方法


🟢 什么是语法树?

语法树(Syntax Tree)是源代码的结构化表示形式。它将源代码文本转换为树形的数据结构,使得程序能够理解和操作代码的结构。

为什么需要语法树?

在传统的文本处理中,代码只是一串字符。但对于编译器、代码分析工具和 IDE 来说,需要理解代码的结构:

  • 哪些是类声明?
  • 哪些是方法?
  • 表达式的计算顺序是什么?
  • 变量的作用域在哪里?

语法树提供了这种结构化的视图,让我们能够:

  1. 分析代码: 查找特定的代码模式
  2. 生成代码: 创建新的代码结构
  3. 重构代码: 修改现有代码的结构
  4. 理解代码: 获取代码的语义信息

简单示例

让我们看一个简单的例子:

csharp
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

这意味着你可以从语法树完全重建原始源代码,包括所有的格式和注释。

csharp
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)

语法树一旦创建就不能修改。如果需要修改,必须创建一个新的树。

为什么要不可变?

  • 线程安全: 多个线程可以安全地读取同一个语法树
  • 可预测性: 不用担心树在使用过程中被意外修改
  • 性能优化: 可以安全地共享和缓存节点
csharp
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; }
IfStatementSyntaxif 语句if (x > 0) { }
ForStatementSyntaxfor 循环for (int i = 0; i < 10; i++) { }
InvocationExpressionSyntax方法调用Console.WriteLine("Hello")
BinaryExpressionSyntax二元表达式a + b

使用示例

csharp
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
符号分隔符和括号{, }, (, ), ;

使用示例

csharp
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

使用示例

csharp
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)。


🟢 语法树示例

让我们通过一个完整的示例来理解语法树的结构。

源代码

csharp
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

遍历示例

csharp
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 提供了多种方式来创建和解析语法树。

从文本创建

最简单的方式是从字符串创建语法树:

csharp
using Microsoft.CodeAnalysis.CSharp;

var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

从文件解析

从文件读取并解析:

csharp
using System.IO;
using Microsoft.CodeAnalysis.CSharp;

var filePath = "Person.cs";
var code = File.ReadAllText(filePath);
var tree = CSharpSyntaxTree.ParseText(code, path: filePath);

提示

指定 path 参数可以让错误消息包含文件名,便于调试。

使用解析选项

可以指定解析选项来控制解析行为:

csharp
var options = new CSharpParseOptions(
    languageVersion: LanguageVersion.CSharp10,
    kind: SourceCodeKind.Regular,
    documentationMode: DocumentationMode.Parse
);

var tree = CSharpSyntaxTree.ParseText(code, options);

常用选项:

选项说明
languageVersionC# 语言版本CSharp7, CSharp8, CSharp9, CSharp10, CSharp11
kind代码类型Regular(普通代码), Script(脚本代码)
documentationMode文档注释模式Parse(解析), Diagnose(诊断), None(忽略)

增量解析

Roslyn 支持增量解析,可以高效地处理代码修改:

csharp
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 可以解析包含语法错误的代码:

csharp
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

导航方法

csharp
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}");
}

位置信息

csharp
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}");

文本内容

csharp
// 获取节点的文本(不含 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}");

🟢 查询和过滤节点

掌握查询和过滤技巧是高效使用语法树的关键。

基本查询

查找特定类型的节点

csharp
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>();

查找第一个匹配的节点

csharp
// 查找第一个类
var firstClass = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .FirstOrDefault();

// 查找第一个公共方法
var firstPublicMethod = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .FirstOrDefault(m => m.Modifiers.Any(
        mod => mod.IsKind(SyntaxKind.PublicKeyword)));

条件过滤

按修饰符过滤

csharp
// 查找所有公共类
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)));

按名称过滤

csharp
// 查找名为 "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"));

按类型过滤

csharp
// 查找所有返回 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 进行复杂查询

组合多个条件

csharp
// 查找所有公共的、非静态的、返回 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");

分组和统计

csharp
// 按返回类型分组方法
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}");
    }
}

查找嵌套结构

csharp
// 查找所有嵌套类
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());

高级查询技巧

使用谓词函数

csharp
// 定义一个谓词函数
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);

递归查询

csharp
// 查找所有包含特定方法调用的方法
var methodsCallingConsole = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .Where(m => m.DescendantNodes()
        .OfType<InvocationExpressionSyntax>()
        .Any(inv => inv.Expression.ToString().StartsWith("Console")));

性能优化技巧

性能提示

  1. 一次遍历收集多种信息: 避免多次调用 DescendantNodes()
  2. 使用 FirstOrDefault() 而不是 First(): 避免在找不到时抛出异常
  3. 缓存查询结果: 如果需要多次使用,先转换为列表
  4. 使用 OfType<T>() 而不是 Where(n => n is T): 性能更好
csharp
// ❌ 不好:多次遍历
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 文档

C# 特定语法节点

工具和资源


📚 下一步

现在你已经掌握了语法树的基础知识,可以继续学习:

  • 遍历方法 - 学习更高级的遍历技术,包括 SyntaxWalker 和 SyntaxRewriter
  • 修改操作 - 学习如何修改语法树,进行代码转换和重构
  • 性能优化 - 深入理解红绿树架构和性能优化技巧

或者返回 语法树概览 查看快速参考和速查表。


实践建议

  • 使用 Roslyn Quoter 来可视化你的代码的语法树结构
  • 在 Visual Studio 中安装 Syntax Visualizer 扩展来实时查看语法树
  • 尝试编写一些简单的代码分析工具来练习查询和过滤技巧

基于 MIT 许可发布