语法树修改操作
简介
本文档详细介绍如何修改 Roslyn 语法树。你将学习不可变性原则、基本修改操作、以及如何添加、删除和替换节点,同时保留代码格式和注释。
适合人群: 中级到高级开发者
预计阅读时间: 75 分钟
前置知识: 语法树基础、遍历方法
📋 本文导航
| 章节 | 难度 | 阅读时间 | 链接 |
|---|---|---|---|
| 不可变性原则 | 🟡 中级 | 10 分钟 | 查看 |
| 基本修改操作 | 🟡 中级 | 15 分钟 | 查看 |
| 添加节点 | 🟡 中级 | 20 分钟 | 查看 |
| 删除节点 | 🟡 中级 | 15 分钟 | 查看 |
| 替换节点 | 🟡 中级 | 15 分钟 | 查看 |
| 批量修改 | 🔴 高级 | 20 分钟 | 查看 |
| 保留格式和注释 | 🔴 高级 | 10 分钟 | 查看 |
返回: 语法树概览 | 上一篇: 遍历方法 | 下一篇: 性能优化
🟡 不可变性原则
Roslyn 语法树是不可变的,这是理解修改操作的关键。
什么是不可变性?
一旦创建,语法树和节点就不能被修改。所有的"修改"操作实际上都是创建新的树或节点。
为什么要不可变?
优点:
- 线程安全: 多个线程可以安全地读取同一个树
- 可预测性: 不用担心树在使用过程中被意外修改
- 性能优化: 可以安全地共享和缓存节点
- 历史追踪: 可以保留修改前后的版本
示例:
csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// ❌ 不能这样做(没有这样的方法)
// classDecl.Identifier = SyntaxFactory.Identifier("Employee");
// ✅ 正确做法:创建新节点
var newClassDecl = classDecl.WithIdentifier(
SyntaxFactory.Identifier("Employee"));
// 原始节点保持不变
Console.WriteLine(classDecl.Identifier.Text); // "Person"
Console.WriteLine(newClassDecl.Identifier.Text); // "Employee"修改操作的基本模式
核心概念
修改语法树的三步骤:
- 找到要修改的节点
- 创建修改后的新节点
- 用新节点替换原节点,得到新树
🟡 基本修改操作
Roslyn 提供了多种方法来修改节点的不同部分。
With* 方法系列
每个语法节点都有一系列 With* 方法,用于创建修改后的副本。
csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 修改标识符
var newClass1 = classDecl.WithIdentifier(
SyntaxFactory.Identifier("Employee"));
// 修改修饰符
var newClass2 = classDecl.WithModifiers(
SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.InternalKeyword)));
// 修改基类列表
var newClass3 = classDecl.WithBaseList(
SyntaxFactory.BaseList(
SyntaxFactory.SingletonSeparatedList<BaseTypeSyntax>(
SyntaxFactory.SimpleBaseType(
SyntaxFactory.IdentifierName("IDisposable")))));ReplaceNode 方法
找到节点后,使用 ReplaceNode 在树中替换它。
csharp
var code = @"
public class Person
{
public string Name { get; set; }
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 找到类声明
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 创建新的类声明(修改名称)
var newClassDecl = classDecl.WithIdentifier(
SyntaxFactory.Identifier("Employee"));
// 在树中替换
var newRoot = root.ReplaceNode(classDecl, newClassDecl);
Console.WriteLine(newRoot.ToFullString());ReplaceToken 方法
替换单个 token:
csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 找到 public 关键字
var publicToken = root.DescendantTokens()
.First(t => t.IsKind(SyntaxKind.PublicKeyword));
// 替换为 internal
var internalToken = SyntaxFactory.Token(SyntaxKind.InternalKeyword)
.WithTriviaFrom(publicToken); // 保留格式
var newRoot = root.ReplaceToken(publicToken, internalToken);
Console.WriteLine(newRoot.ToFullString());
// 输出: internal class Person { }注意
使用 ReplaceNode 或 ReplaceToken 后,原始的节点引用将失效。如果需要继续修改,使用新树中的节点。
🟡 添加节点
向类、命名空间等容器节点添加成员。
添加方法到类
csharp
var code = @"
public class Calculator
{
public int Add(int a, int b) => a + b;
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 创建新方法
var newMethod = SyntaxFactory.MethodDeclaration(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.IntKeyword)),
"Subtract")
.WithModifiers(
SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
.WithParameterList(
SyntaxFactory.ParameterList(
SyntaxFactory.SeparatedList(new[]
{
SyntaxFactory.Parameter(
SyntaxFactory.Identifier("a"))
.WithType(SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.IntKeyword))),
SyntaxFactory.Parameter(
SyntaxFactory.Identifier("b"))
.WithType(SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.IntKeyword)))
})))
.WithExpressionBody(
SyntaxFactory.ArrowExpressionClause(
SyntaxFactory.BinaryExpression(
SyntaxKind.SubtractExpression,
SyntaxFactory.IdentifierName("a"),
SyntaxFactory.IdentifierName("b"))))
.WithSemicolonToken(
SyntaxFactory.Token(SyntaxKind.SemicolonToken));
// 添加方法到类
var newClassDecl = classDecl.AddMembers(newMethod);
// 替换类
var newRoot = root.ReplaceNode(classDecl, newClassDecl);
Console.WriteLine(newRoot.ToFullString());使用 ParseStatement 简化
点击查看简化的添加方法
csharp
// 更简单的方式:解析代码字符串
var methodCode = @"
public int Subtract(int a, int b) => a - b;
";
var newMethod = SyntaxFactory.ParseMemberDeclaration(methodCode);
var newClassDecl = classDecl.AddMembers(newMethod);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);添加属性
csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 解析属性代码
var property = SyntaxFactory.ParseMemberDeclaration(
"public string Name { get; set; }");
// 添加属性
var newClassDecl = classDecl.AddMembers(property);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);
Console.WriteLine(newRoot.ToFullString());添加多个成员
csharp
var properties = new[]
{
"public string Name { get; set; }",
"public int Age { get; set; }",
"public string Email { get; set; }"
};
var members = properties
.Select(p => SyntaxFactory.ParseMemberDeclaration(p))
.ToArray();
var newClassDecl = classDecl.AddMembers(members);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);提示
使用 ParseMemberDeclaration 或 ParseStatement 可以大大简化代码生成,避免手动构建复杂的语法树。
🟡 删除节点
从树中删除节点。
删除方法
csharp
var code = @"
public class Calculator
{
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
public int Multiply(int a, int b) => a * b;
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// 找到要删除的方法
var methodToRemove = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.First(m => m.Identifier.Text == "Subtract");
// 删除节点
var newRoot = root.RemoveNode(methodToRemove, SyntaxRemoveOptions.KeepNoTrivia);
Console.WriteLine(newRoot.ToFullString());SyntaxRemoveOptions
删除节点时可以指定如何处理 trivia(空格、注释等):
| 选项 | 说明 |
|---|---|
KeepNoTrivia | 不保留任何 trivia |
KeepLeadingTrivia | 保留前导 trivia |
KeepTrailingTrivia | 保留尾随 trivia |
KeepDirectives | 保留预处理指令 |
KeepEndOfLine | 保留换行符 |
KeepExteriorTrivia | 保留外部 trivia |
KeepUnbalancedDirectives | 保留不平衡的指令 |
csharp
// 保留注释
var newRoot1 = root.RemoveNode(methodToRemove,
SyntaxRemoveOptions.KeepLeadingTrivia);
// 保留所有格式
var newRoot2 = root.RemoveNode(methodToRemove,
SyntaxRemoveOptions.KeepExteriorTrivia |
SyntaxRemoveOptions.KeepEndOfLine);删除多个节点
csharp
// 删除所有私有方法
var privateMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.Modifiers.Any(
mod => mod.IsKind(SyntaxKind.PrivateKeyword)))
.ToList();
var newRoot = root.RemoveNodes(privateMethods,
SyntaxRemoveOptions.KeepNoTrivia);条件删除
点击查看条件删除示例
csharp
// 删除所有空方法
var emptyMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.Body != null &&
m.Body.Statements.Count == 0)
.ToList();
if (emptyMethods.Any())
{
var newRoot = root.RemoveNodes(emptyMethods,
SyntaxRemoveOptions.KeepNoTrivia);
Console.WriteLine(newRoot.ToFullString());
}🟡 替换节点
替换节点是最常用的修改操作。
简单替换
csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 创建新节点
var newClassDecl = classDecl.WithIdentifier(
SyntaxFactory.Identifier("Employee"));
// 替换
var newRoot = root.ReplaceNode(classDecl, newClassDecl);批量替换
csharp
// 将所有 public 方法改为 private
var publicMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.Modifiers.Any(
mod => mod.IsKind(SyntaxKind.PublicKeyword)))
.ToList();
var newRoot = root.ReplaceNodes(
publicMethods,
(oldNode, newNode) =>
{
var publicModifier = newNode.Modifiers
.First(m => m.IsKind(SyntaxKind.PublicKeyword));
var privateModifier = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
.WithTriviaFrom(publicModifier);
return newNode.WithModifiers(
newNode.Modifiers.Replace(publicModifier, privateModifier));
});条件替换
点击查看条件替换示例
csharp
// 为所有没有访问修饰符的方法添加 public
var methodsWithoutModifiers = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => !m.Modifiers.Any())
.ToList();
var newRoot = root.ReplaceNodes(
methodsWithoutModifiers,
(oldNode, newNode) =>
{
return newNode.WithModifiers(
SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PublicKeyword)));
});替换表达式
csharp
// 将所有 a + b 替换为 Add(a, b)
var additions = root.DescendantNodes()
.OfType<BinaryExpressionSyntax>()
.Where(b => b.IsKind(SyntaxKind.AddExpression))
.ToList();
var newRoot = root.ReplaceNodes(
additions,
(oldNode, newNode) =>
{
return SyntaxFactory.InvocationExpression(
SyntaxFactory.IdentifierName("Add"),
SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList(new[]
{
SyntaxFactory.Argument(newNode.Left),
SyntaxFactory.Argument(newNode.Right)
})));
});🔴 批量修改
使用 SyntaxRewriter 进行批量修改是最强大的方式。
创建 Rewriter
csharp
public class MethodModifierRewriter : CSharpSyntaxRewriter
{
private readonly SyntaxKind _fromModifier;
private readonly SyntaxKind _toModifier;
public MethodModifierRewriter(
SyntaxKind fromModifier,
SyntaxKind toModifier)
{
_fromModifier = fromModifier;
_toModifier = toModifier;
}
public override SyntaxNode VisitMethodDeclaration(
MethodDeclarationSyntax node)
{
var modifier = node.Modifiers
.FirstOrDefault(m => m.IsKind(_fromModifier));
if (modifier != default)
{
var newModifier = SyntaxFactory.Token(_toModifier)
.WithTriviaFrom(modifier);
var newModifiers = node.Modifiers.Replace(
modifier, newModifier);
return node.WithModifiers(newModifiers);
}
return base.VisitMethodDeclaration(node);
}
}
// 使用
var rewriter = new MethodModifierRewriter(
SyntaxKind.PublicKeyword,
SyntaxKind.PrivateKeyword);
var newRoot = rewriter.Visit(root);复杂的 Rewriter 示例
点击查看日志注入 Rewriter
csharp
public class LoggingRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(
MethodDeclarationSyntax node)
{
// 只处理公共方法
if (!node.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)))
return base.VisitMethodDeclaration(node);
// 只处理有方法体的方法
if (node.Body == null)
return base.VisitMethodDeclaration(node);
// 创建日志语句
var logStatement = SyntaxFactory.ParseStatement(
$"Console.WriteLine(\"[LOG] 进入方法: {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 UserService
{
public void CreateUser(string name)
{
// 创建用户逻辑
}
public void DeleteUser(int id)
{
// 删除用户逻辑
}
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var rewriter = new LoggingRewriter();
var newRoot = rewriter.Visit(root);
Console.WriteLine(newRoot.ToFullString());链式 Rewriter
csharp
// 可以链式应用多个 rewriter
var root1 = new LoggingRewriter().Visit(root);
var root2 = new MethodModifierRewriter(
SyntaxKind.PublicKeyword,
SyntaxKind.InternalKeyword).Visit(root1);
var root3 = new CommentAdder().Visit(root2);🔴 保留格式和注释
修改语法树时保留原有的格式和注释很重要。
使用 WithTriviaFrom
csharp
var code = @"
// 这是一个重要的类
public class Person
{
// 姓名属性
public string Name { get; set; }
}";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.First();
// 修改类名,但保留注释
var newIdentifier = SyntaxFactory.Identifier("Employee")
.WithTriviaFrom(classDecl.Identifier);
var newClassDecl = classDecl.WithIdentifier(newIdentifier);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);
Console.WriteLine(newRoot.ToFullString());
// 注释被保留保留前导和尾随 Trivia
csharp
// 保留前导 trivia(注释、空格等)
var newToken = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
.WithLeadingTrivia(oldToken.LeadingTrivia)
.WithTrailingTrivia(oldToken.TrailingTrivia);
// 或者使用 WithTriviaFrom 一次性复制
var newToken2 = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
.WithTriviaFrom(oldToken);添加注释
csharp
// 添加单行注释
var comment = SyntaxFactory.Comment("// 这是新添加的注释\n");
var newMethod = method.WithLeadingTrivia(
method.GetLeadingTrivia().Add(comment));
// 添加文档注释
var docComment = SyntaxFactory.ParseLeadingTrivia(@"
/// <summary>
/// 这是一个新方法
/// </summary>
").ToList();
var newMethod2 = method.WithLeadingTrivia(docComment);格式化代码
点击查看代码格式化示例
csharp
using Microsoft.CodeAnalysis.Formatting;
// 使用 Formatter 格式化代码
var workspace = new AdhocWorkspace();
var formattedRoot = Formatter.Format(
newRoot,
workspace);
Console.WriteLine(formattedRoot.ToFullString());最佳实践
- 使用
WithTriviaFrom保留格式 - 使用
Formatter.Format格式化生成的代码 - 删除节点时选择合适的
SyntaxRemoveOptions - 添加新节点时考虑添加适当的空格和换行
🔗 相关 API
核心 API 文档
- SyntaxNode.ReplaceNode - 替换节点
- SyntaxNode.RemoveNode - 删除节点
- CSharpSyntaxRewriter - 语法树重写器
- SyntaxFactory - 创建语法节点
- Formatter - 代码格式化
相关文档
📚 下一步
现在你已经掌握了修改语法树的方法,可以继续学习:
- 性能优化 - 深入理解性能优化和最佳实践
或者返回 语法树概览 查看快速参考。
实践建议
- 始终记住语法树是不可变的
- 使用
ParseMemberDeclaration简化代码生成 - 使用
WithTriviaFrom保留格式 - 使用 SyntaxRewriter 进行批量修改
- 使用 Roslyn Quoter 来生成 SyntaxFactory 代码