使用 CodeDomVisitor 搜索和更改代码





5.00/5 (3投票s)
在 CodeDOM 上实现访问者模式
引言
如果您曾经在 .NET 中编写过代码生成工具,您可能遇到过 CodeDOM - 一种抽象语法树,用于表示通用的代码结构,如条件语句、表达式、迭代语句、方法、属性、字段等。这些代码结构可以用任何目标语言呈现。
有时,您必须润色树,要么是因为在解析它时没有足够的信息(就像我的 Slang 解析器),要么是因为您从第三方获取了它,比如 Microsoft 的 XML 序列化 API。
如果没有合适的工具,这会很快变得非常困难。输入 CodeDomVisitor
。此对象在代码树上实现 访问者模式,允许您相对轻松地修改和搜索它。
背景
访问者是允许您遍历第三方对象模型的对象,依次访问其所有对象,一个接一个地报告它们,通常使用递归下降来遍历树。通常,它们是硬编码实现的,只需手动编写所有单独的调用,就像这样,或者它们可以使用反射。反射比较慢,所以我采用了硬编码的方式,尽管需要额外的工作。
Using the Code
使用代码进行搜索非常简单,所以我们将进行搜索和替换以保持趣味性。访问演示代码示例 1,我们有一个 CodeDOM,表示以下代码
internal class Program {
public static void Main(string[] args) {
System.Console.WriteLine("Hello World!");
}
}
对于我们的示例,上面在名为 cls
的 CodeTypeDeclaration
对象中表示。下面,我们将进行一个简单的替换,将字符串 "Hello World!"
替换为另一个字符串
// replace "Hello World!" with "Goodbye Cruel World!"
CodeDomVisitor.Visit(cls, (ctx) => {
var cp = ctx.Target as CodePrimitiveExpression;
if(null!=cp)
{
if(0==string.Compare("Hello World!",cp.Value as string))
{
// update the field
cp.Value = "Goodbye Cruel World!";
}
}
});
您的匿名方法将为每个对象调用一次,其中包含一个上下文参数,我们在上面称之为 ctx
,它包含访问的根 (ctx.Root
),其中包含最初传递给 Visit()
的对象,父项 (ctx.Parent
),父项上包含该项的成员 (ctx.Member
),以及目标项本身 (ctx.Target
)。它还包含一个 Targets
属性,允许您获取或设置正在访问的目标类型。在这里要小心,因为如果您只访问成员,例如,访问者将永远无法到达树的许多部分,因此请仔细选择您的目标。它拥有的最后一个属性是 Cancel
,在对树进行任何重大修改后,您应该将其设置为 true
- 参见演示中包含的注释。取消会立即停止访问并报告不再有任何项。
上面很简单。我们只是访问,寻找一个值为 "Hello World!"
的 CodePrimitiveExpression
,然后我们进行更改。这样做根本没有改变访问的路径,所以我们不必取消访问,尽管我们可以简单地因为我们已经找到了结果。由于我们没有,这将更改它找到的每个目标,而不仅仅是第一个。取消显然会更有效率。我们将在下面的第二个示例中深入探讨取消
CodeDomVisitor.Visit(cls, (ctx) => {
var cp = ctx.Target as CodePrimitiveExpression;
if (null != cp)
{
if (0 == string.Compare("Hello World!", cp.Value as string))
{
var newObj = new CodeArrayIndexerExpression(
new CodeArgumentReferenceExpression("args"),
new CodePrimitiveExpression(0));
// use reflection to change the parent object.
// since we're changing the parent, we should
// cancel visiting because what we're visiting
// is based on a path that no longer is in the
// tree once this is finished. It's all orphaned.
// ctx.Parent contains the parent object of this
// visit and ctx.Member contains the member that
// was used. Sometimes this can be a collection.
// We'll want to handle that, replacing the old
// item in the collection with the new item.
// Members from the CodeDOM should always return
// the first member named when reflected so this
// is straightforward, if ugly:
var member = ctx.Parent.GetType().GetMember(ctx.Member)[0] as PropertyInfo;
if(typeof(System.Collections.IList).IsAssignableFrom(member.PropertyType))
{
// this is a collection
var l = member.GetValue(ctx.Parent) as System.Collections.IList;
var i = l.IndexOf(ctx.Target);
l[i] = newObj;
}
else // scalar value - we just set here
// (not used in the demo, provided for completeness)
member.SetValue(
ctx.Parent,
newObj);
ctx.Cancel = true;
}
}
});
这更复杂一些,因为我们试图更新我们正在访问的父节点,用其他东西替换我们自己的节点。首先,请记住在执行此操作时取消。有时,我在一个循环中使用 while(more)
运行 Visit()
,在取消之前,我将 more 设置为 true,并重新访问一棵“新鲜”树。如果您要执行多次替换,则应牢记这种模式。这里的另一个复杂因素是反射,但这是另一个主题。基本上,我们在这里做的是寻找 Member
指示的属性,检查它是否是一个集合,如果是,我们只需用新的成员替换旧的成员。如果它不是集合,我们只需将该属性设置为我们的新值。
就是这样。除了实现之外,这里没有什么可探索的了,它很丑陋,但很直接。它只是依次访问所有属性。
历史
- 2019 年 12 月 6 日 - 初始提交