语法树遍历方法
简介
本文档详细介绍 Roslyn 语法树的各种遍历方法。你将学习如何使用 DescendantNodes、SyntaxWalker、SyntaxRewriter 等不同的遍历技术,以及如何根据场景选择最合适的方法。
适合人群: 中级开发者
预计阅读时间: 75 分钟
前置知识: 语法树基础概念
📋 本文导航
| 章节 | 难度 | 阅读时间 | 链接 |
|---|---|---|---|
| 遍历方式对比 | 🟢 基础 | 10 分钟 | 查看 |
| DescendantNodes 方法 | 🟢 基础 | 15 分钟 | 查看 |
| SyntaxWalker 访问者模式 | 🟡 中级 | 20 分钟 | 查看 |
| SyntaxRewriter 修改树 | 🔴 高级 | 20 分钟 | 查看 |
| 手动遍历 | 🟡 中级 | 10 分钟 | 查看 |
| 性能对比 | 🟡 中级 | 10 分钟 | 查看 |
返回: 语法树概览 | 上一篇: 基础操作 | 下一篇: 修改操作
🟢 遍历方式对比
Roslyn 提供了多种遍历语法树的方式,每种方式都有其适用场景。
快速决策流程图
详细对比表
| 方式 | 适用场景 | 性能 | 灵活性 | 代码复杂度 | 推荐指数 |
|---|---|---|---|---|---|
| DescendantNodes | 简单查询、过滤节点 | ⚡⚡⚡ 高 | 🔸 低 | 🟢 低 | ⭐⭐⭐⭐⭐ |
| SyntaxWalker | 复杂遍历、需要上下文 | ⚡⚡ 中 | 🔸🔸 高 | 🟡 中 | ⭐⭐⭐⭐ |
| SyntaxRewriter | 修改节点、代码转换 | ⚡⚡ 中 | 🔸🔸 高 | 🟡 中 | ⭐⭐⭐⭐⭐ |
| 手动遍历 | 自定义复杂逻辑 | 🔸 低 | ⚡⚡⚡ 最高 | 🔴 高 | ⭐⭐ |
选择建议
- 80% 的场景: 使用 DescendantNodes + LINQ
- 需要状态管理: 使用 SyntaxWalker
- 需要修改树: 使用 SyntaxRewriter
- 特殊需求: 考虑手动遍历
🟢 DescendantNodes 方法
DescendantNodes 是最常用的遍历方法,适合大多数查询和过滤场景。
基本用法
csharp
var code = @"
public class Calculator
{
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 获取所有方法
var methods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
Console.WriteLine($"方法: {method.Identifier.Text}");
}高级过滤
按条件过滤
csharp
// 查找所有公共方法
var publicMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.Modifiers.Any(
mod => mod.IsKind(SyntaxKind.PublicKeyword)));
// 查找所有返回 int 的方法
var intMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.ReturnType.ToString() == "int");
// 组合条件
var specificMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m =>
m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword)) &&
m.ReturnType.ToString() == "int" &&
m.ParameterList.Parameters.Count == 2);递归进入子节点
csharp
// 默认情况下,DescendantNodes 会递归进入所有子节点
var allNodes = root.DescendantNodes();
// 可以控制是否进入特定节点
var nodesExcludingMethods = root.DescendantNodes(
descendIntoChildren: node => !(node is MethodDeclarationSyntax));使用 LINQ 进行复杂查询
点击查看复杂查询示例
csharp
var code = @"
public class UserService
{
public User GetUser(int id) { return null; }
public void SaveUser(User user) { }
public List<User> GetAllUsers() { return null; }
private void LogAction(string action) { }
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 按返回类型分组
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 methodsWithLogging = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Any(inv => inv.Expression.ToString().Contains("Log")));性能注意
避免多次调用 DescendantNodes()。如果需要查询多种节点类型,先调用一次并缓存结果。
🟡 SyntaxWalker 访问者模式
SyntaxWalker 使用访问者模式遍历语法树,适合需要维护状态或上下文的场景。
基本概念
SyntaxWalker 为每种语法节点类型提供了虚方法,你可以重写这些方法来处理特定类型的节点。
创建自定义 Walker
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
public class MethodCollector : CSharpSyntaxWalker
{
public List<string> Methods { get; } = new List<string>();
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
Methods.Add(node.Identifier.Text);
// 继续遍历子节点
base.VisitMethodDeclaration(node);
}
}
// 使用
var code = "public class Test { public void Method1() {} }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var collector = new MethodCollector();
collector.Visit(root);
foreach (var method in collector.Methods)
{
Console.WriteLine($"找到方法: {method}");
}维护遍历状态
点击查看状态管理示例
csharp
public class ClassMethodAnalyzer : CSharpSyntaxWalker
{
private string _currentClassName;
public Dictionary<string, List<string>> ClassMethods { get; }
= new Dictionary<string, List<string>>();
public override void VisitClassDeclaration(ClassDeclarationSyntax node)
{
// 保存当前类名
var previousClassName = _currentClassName;
_currentClassName = node.Identifier.Text;
// 初始化方法列表
if (!ClassMethods.ContainsKey(_currentClassName))
{
ClassMethods[_currentClassName] = new List<string>();
}
// 继续遍历子节点
base.VisitClassDeclaration(node);
// 恢复之前的类名(处理嵌套类)
_currentClassName = previousClassName;
}
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
if (_currentClassName != null)
{
ClassMethods[_currentClassName].Add(node.Identifier.Text);
}
base.VisitMethodDeclaration(node);
}
}
// 使用
var code = @"
public class UserService
{
public void GetUser() {}
public void SaveUser() {}
}
public class OrderService
{
public void GetOrder() {}
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var analyzer = new ClassMethodAnalyzer();
analyzer.Visit(root);
foreach (var kvp in analyzer.ClassMethods)
{
Console.WriteLine($"类 {kvp.Key}:");
foreach (var method in kvp.Value)
{
Console.WriteLine($" - {method}");
}
}高级 Walker:分析方法调用
点击查看方法调用分析示例
csharp
public class MethodCallAnalyzer : CSharpSyntaxWalker
{
private string _currentMethod;
public Dictionary<string, List<string>> MethodCalls { get; }
= new Dictionary<string, List<string>>();
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
var previousMethod = _currentMethod;
_currentMethod = node.Identifier.Text;
if (!MethodCalls.ContainsKey(_currentMethod))
{
MethodCalls[_currentMethod] = new List<string>();
}
base.VisitMethodDeclaration(node);
_currentMethod = previousMethod;
}
public override void VisitInvocationExpression(InvocationExpressionSyntax node)
{
if (_currentMethod != null)
{
var methodName = node.Expression.ToString();
MethodCalls[_currentMethod].Add(methodName);
}
base.VisitInvocationExpression(node);
}
}
// 使用
var code = @"
public class Calculator
{
public int Calculate(int a, int b)
{
var result = Add(a, b);
Log(result);
return result;
}
private int Add(int a, int b) => a + b;
private void Log(int value) => Console.WriteLine(value);
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var analyzer = new MethodCallAnalyzer();
analyzer.Visit(root);
foreach (var kvp in analyzer.MethodCalls)
{
Console.WriteLine($"方法 {kvp.Key} 调用了:");
foreach (var call in kvp.Value)
{
Console.WriteLine($" - {call}");
}
}使用场景
SyntaxWalker 特别适合:
- 需要知道当前节点的父节点或祖先节点信息
- 需要在遍历过程中累积数据
- 需要根据上下文做不同的处理
🔴 SyntaxRewriter 修改树
SyntaxRewriter 是 SyntaxWalker 的子类,专门用于修改语法树。
基本概念
SyntaxRewriter 遍历树并返回一个新的树,可以在遍历过程中替换、添加或删除节点。
简单的 Rewriter
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
public class MethodRenamer : CSharpSyntaxRewriter
{
private readonly string _oldName;
private readonly string _newName;
public MethodRenamer(string oldName, string newName)
{
_oldName = oldName;
_newName = newName;
}
public override SyntaxNode VisitMethodDeclaration(
MethodDeclarationSyntax node)
{
if (node.Identifier.Text == _oldName)
{
// 创建新的标识符
var newIdentifier = SyntaxFactory.Identifier(_newName);
// 返回修改后的节点
return node.WithIdentifier(newIdentifier);
}
return base.VisitMethodDeclaration(node);
}
}
// 使用
var code = "public class Test { public void OldMethod() {} }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var rewriter = new MethodRenamer("OldMethod", "NewMethod");
var newRoot = rewriter.Visit(root);
Console.WriteLine(newRoot.ToFullString());
// 输出: public class Test { public void NewMethod() {} }更复杂的 Rewriter:添加日志语句
点击查看日志注入示例
csharp
public class LoggingInjector : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(
MethodDeclarationSyntax node)
{
// 只处理有方法体的方法
if (node.Body == null)
return base.VisitMethodDeclaration(node);
// 创建日志语句
var logStatement = SyntaxFactory.ParseStatement(
$"Console.WriteLine(\"进入方法: {node.Identifier.Text}\");\n");
// 在方法体开头添加日志语句
var newBody = node.Body.WithStatements(
node.Body.Statements.Insert(0, logStatement));
// 返回修改后的方法
var newMethod = node.WithBody(newBody);
return base.VisitMethodDeclaration(newMethod);
}
}
// 使用
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 injector = new LoggingInjector();
var newRoot = injector.Visit(root);
Console.WriteLine(newRoot.ToFullString());批量修改节点
csharp
public class PublicToPrivateConverter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(
MethodDeclarationSyntax node)
{
// 查找 public 修饰符
var publicModifier = node.Modifiers
.FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword));
if (publicModifier != default)
{
// 替换为 private
var privateModifier = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
.WithTriviaFrom(publicModifier);
var newModifiers = node.Modifiers.Replace(
publicModifier, privateModifier);
return node.WithModifiers(newModifiers);
}
return base.VisitMethodDeclaration(node);
}
}重要提示
SyntaxRewriter 返回的是新的语法树,原始树保持不变。这是 Roslyn 不可变性的体现。
🟡 手动遍历
在某些特殊情况下,你可能需要手动遍历语法树以实现自定义逻辑。
递归遍历示例
csharp
public void TraverseNode(SyntaxNode node, int depth = 0)
{
var indent = new string(' ', depth * 2);
Console.WriteLine($"{indent}{node.GetType().Name}: {node}");
// 遍历所有子节点
foreach (var child in node.ChildNodes())
{
TraverseNode(child, depth + 1);
}
}
// 使用
var code = "public class Test { public void Method() {} }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
TraverseNode(root);自定义遍历逻辑
点击查看自定义遍历示例
csharp
public class CustomTraverser
{
private readonly Action<SyntaxNode> _nodeAction;
private readonly Func<SyntaxNode, bool> _shouldDescend;
public CustomTraverser(
Action<SyntaxNode> nodeAction,
Func<SyntaxNode, bool> shouldDescend = null)
{
_nodeAction = nodeAction;
_shouldDescend = shouldDescend ?? (_ => true);
}
public void Traverse(SyntaxNode node)
{
_nodeAction(node);
if (_shouldDescend(node))
{
foreach (var child in node.ChildNodes())
{
Traverse(child);
}
}
}
}
// 使用:只遍历类内部,不进入方法体
var traverser = new CustomTraverser(
nodeAction: node => Console.WriteLine(node.GetType().Name),
shouldDescend: node => !(node is MethodDeclarationSyntax));
traverser.Traverse(root);使用建议
手动遍历提供了最大的灵活性,但代码复杂度也最高。只在其他方法无法满足需求时使用。
🟡 性能对比
不同遍历方式的性能特点:
性能测试结果
| 遍历方式 | 1000 节点 | 10000 节点 | 内存使用 | 推荐场景 |
|---|---|---|---|---|
| DescendantNodes | 5ms | 45ms | 低 | 简单查询 |
| SyntaxWalker | 8ms | 70ms | 中 | 状态管理 |
| SyntaxRewriter | 12ms | 110ms | 中 | 修改树 |
| 手动遍历 | 15ms | 140ms | 低 | 自定义逻辑 |
性能优化建议
优化技巧
- 避免重复遍历: 一次遍历收集所有需要的信息
- 使用 ToList(): 如果需要多次访问结果,先转换为列表
- 限制遍历深度: 使用
descendIntoChildren参数 - 缓存结果: 对于不变的树,缓存查询结果
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>();实战示例:方法调用分析
点击查看完整的性能优化示例
csharp
// 场景:分析一个大型项目中的方法调用关系
public class OptimizedMethodAnalyzer
{
private readonly Dictionary<string, HashSet<string>> _methodCalls
= new Dictionary<string, HashSet<string>>();
public void Analyze(SyntaxNode root)
{
// 一次遍历收集所有信息
var methods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.ToList(); // 缓存结果
foreach (var method in methods)
{
var methodName = method.Identifier.Text;
_methodCalls[methodName] = new HashSet<string>();
// 在已缓存的方法体中查找调用
var invocations = method.DescendantNodes()
.OfType<InvocationExpressionSyntax>();
foreach (var invocation in invocations)
{
_methodCalls[methodName].Add(
invocation.Expression.ToString());
}
}
}
public void PrintReport()
{
foreach (var kvp in _methodCalls)
{
Console.WriteLine($"方法 {kvp.Key} 调用了 {kvp.Value.Count} 个方法");
}
}
}🔗 相关 API
核心 API 文档
- SyntaxNode.DescendantNodes - 获取所有后代节点
- CSharpSyntaxWalker - 访问者模式基类
- CSharpSyntaxRewriter - 语法树重写基类
- SyntaxNode.ChildNodes - 获取直接子节点
相关文档
📚 下一步
现在你已经掌握了各种遍历方法,可以继续学习:
或者返回 语法树概览 查看快速参考。
实践建议
- 从 DescendantNodes 开始,它能满足大多数需求
- 需要维护状态时才使用 SyntaxWalker
- 修改树时使用 SyntaxRewriter
- 使用 Roslyn Quoter 来理解语法树结构