65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 CodeDomVisitor 搜索和更改代码

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2019 年 12 月 6 日

CPOL

3分钟阅读

viewsIcon

4237

downloadIcon

114

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

对于我们的示例,上面在名为 clsCodeTypeDeclaration 对象中表示。下面,我们将进行一个简单的替换,将字符串 "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 日 - 初始提交
© . All rights reserved.